@code-yeongyu/senpi-codemode 2026.10.4 → 2026.10.6

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +27 -2
  3. package/package.json +4 -4
  4. package/src/bridge/kernel-tools-protocol.ts +1 -1
  5. package/src/bridge/protocol.ts +11 -0
  6. package/src/config/feature-settings.ts +110 -0
  7. package/src/config/memory-settings.ts +8 -0
  8. package/src/config/settings-constants.ts +7 -0
  9. package/src/config/settings-overrides.ts +72 -0
  10. package/src/config/settings.ts +35 -78
  11. package/src/config/unknown-settings.ts +19 -0
  12. package/src/extension/bridge-tool-call.ts +37 -0
  13. package/src/extension/idle-parking-kernel.ts +272 -0
  14. package/src/extension/kernel-registration.ts +25 -0
  15. package/src/extension/kernel-replacement.ts +230 -0
  16. package/src/extension/kernel-tools-probe.ts +24 -0
  17. package/src/extension/session-manager.ts +49 -38
  18. package/src/kernels/jl/kernel.ts +6 -2
  19. package/src/kernels/jl/runner.jl +154 -0
  20. package/src/kernels/js/context-manager.ts +1 -1
  21. package/src/kernels/js/kernel-tools-types.ts +2 -1
  22. package/src/kernels/js/worker-core.js +1 -0
  23. package/src/kernels/js/worker-global-sizes.js +84 -14
  24. package/src/kernels/py/kernel-contract.ts +2 -1
  25. package/src/kernels/py/kernel.ts +48 -8
  26. package/src/kernels/py/prelude.py +6 -1
  27. package/src/kernels/py/transport.ts +25 -4
  28. package/src/kernels/rb/kernel.ts +6 -2
  29. package/src/kernels/rb/runner.rb +141 -0
  30. package/src/kernels/session-env.ts +3 -0
  31. package/src/kernels/shared/kernel-death.ts +39 -0
  32. package/src/kernels/shared/kernel-memory-host.ts +14 -6
  33. package/src/kernels/{js/kernel-tools-host.ts → shared/kernel-tools-pump.ts} +2 -2
  34. package/src/kernels/shared/process-group-cpu.ts +139 -0
  35. package/src/kernels/shared/subprocess-contract.ts +5 -1
  36. package/src/kernels/shared/subprocess-kernel.ts +66 -3
  37. package/src/kernels/shared/subprocess-queue.ts +5 -0
  38. package/src/kernels/shared/subprocess-startup.ts +90 -0
  39. package/src/tool/cell-runtime.ts +17 -4
  40. package/src/tool/detached-cell-notification.ts +13 -2
  41. package/src/tool/run-eval-cell.ts +5 -6
  42. package/src/tool/types.ts +14 -0
@@ -2,21 +2,22 @@ import { join } from "node:path";
2
2
  import type { ExtensionContext } from "@code-yeongyu/senpi";
3
3
  import { type BridgeServerHandle, startBridgeServer } from "../bridge/http-server.ts";
4
4
  import type { KernelToHostMessage } from "../bridge/protocol.ts";
5
- import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
6
5
  import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
7
6
  import { resolveKernelMemoryThresholds } from "../config/memory-settings.ts";
8
- import { defaultCodemodeSettings } from "../config/settings.ts";
9
7
  import { collectOrphanedChildren } from "../host-sdk.ts";
10
8
  import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
11
- import { marshalToolResult } from "../tool/image.ts";
9
+ import type { KernelLifecycle } from "../kernels/shared/kernel-death.ts";
12
10
  import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
11
+ import { type BridgeToolCallRequest, routeBridgeToolCall } from "./bridge-tool-call.ts";
12
+ import { parkWhenIdle } from "./idle-parking-kernel.ts";
13
13
  import {
14
14
  javaScriptKernelMemory,
15
- registerKernel,
15
+ SessionKernelRegistrations,
16
16
  type StartedKernel,
17
17
  startSubprocessKernel,
18
18
  } from "./kernel-registration.ts";
19
- import { kernelRegistry } from "./kernel-registry.ts";
19
+ import type { RegisteredKernelSource } from "./kernel-registry.ts";
20
+ import { ReplaceableKernel } from "./kernel-replacement.ts";
20
21
  import { assertSessionCwdAvailable } from "./session-cwd.ts";
21
22
  import type {
22
23
  BridgeEndpoint,
@@ -59,7 +60,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
59
60
  readonly #options: CreateCodemodeSessionManagerOptions;
60
61
  #bridge: BridgeServerHandle | undefined;
61
62
  #kernels = new Map<EvalLanguage, EvalKernel>();
62
- readonly #registrations = new Map<EvalLanguage, string>();
63
+ readonly #registrations: SessionKernelRegistrations;
63
64
  #kernelCreations = new Map<EvalLanguage, Promise<EvalKernel>>();
64
65
  #onMessageRefs = new Map<EvalLanguage, (message: KernelToHostMessage) => void>();
65
66
  #context: ExtensionContext | undefined;
@@ -68,6 +69,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
68
69
 
69
70
  constructor(options: CreateCodemodeSessionManagerOptions) {
70
71
  this.#options = options;
72
+ this.#registrations = new SessionKernelRegistrations(options.ownerSessionId ?? options.sessionId);
71
73
  }
72
74
 
73
75
  async start(): Promise<void> {
@@ -79,29 +81,8 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
79
81
  });
80
82
  }
81
83
 
82
- // Subprocess kernels (py/rb/jl) reach the host only through this route, so every reply
83
- // must match the in-process JS path in tool/cell-handler.ts: reserved helper names dispatch
84
- // through runReservedTool (forwarding them made agent() fail with "Unknown tool __agent__"),
85
- // and ordinary tool results are marshalled to { text, images, details, hasError } — the raw
86
- // { content } shape left python cells unable to reach tool.read image blocks.
87
- async #call(request: { toolName: string; args: unknown; callId: string; signal: AbortSignal }): Promise<unknown> {
88
- if (!isReservedToolName(request.toolName)) {
89
- return marshalToolResult(
90
- await this.#options.executeTool(request.toolName, request.args, { signal: request.signal }),
91
- );
92
- }
93
- const taskTools = this.#options.settings.taskTools ?? defaultCodemodeSettings.taskTools;
94
- return await runReservedTool(request.toolName, {
95
- callId: request.callId,
96
- args: request.args,
97
- executeTool: this.#options.executeTool,
98
- taskToolName: taskTools.task,
99
- taskOutputToolName: taskTools.output,
100
- listTools: this.#options.listTools,
101
- signal: request.signal,
102
- emitStatus: () => {},
103
- marshalToolResult,
104
- });
84
+ async #call(request: BridgeToolCallRequest): Promise<unknown> {
85
+ return await routeBridgeToolCall(this.#options, request);
105
86
  }
106
87
 
107
88
  async getKernel(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): Promise<EvalKernel> {
@@ -168,7 +149,6 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
168
149
  const kernels = [...this.#kernels.values()];
169
150
  const bridge = this.#bridge;
170
151
  this.#kernels.clear();
171
- for (const id of this.#registrations.values()) kernelRegistry.unregister(id);
172
152
  this.#registrations.clear();
173
153
  this.#onMessageRefs.clear();
174
154
  this.#bridge = undefined;
@@ -192,27 +172,57 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
192
172
  generation: number,
193
173
  ): Promise<EvalKernel> {
194
174
  await assertSessionCwdAvailable(this.#options.cwd);
195
- const { kernel, memory } = await this.#createKernel(language, onMessage);
175
+ const started = await this.#startLanguageKernel(language, onMessage);
176
+ const idleParkMinutes = this.#options.settings.memory?.idleParkMinutes ?? 0;
177
+ const resolvedKernel = parkWhenIdle(
178
+ language,
179
+ idleParkMinutes,
180
+ started.kernel,
181
+ async () => {
182
+ const restarted = await this.#startLanguageKernel(language, onMessage);
183
+ if (generation !== this.#generation) {
184
+ await restarted.kernel.close();
185
+ throw new CodemodeSessionDisposedError();
186
+ }
187
+ this.#registrations.register(language, restarted.memory);
188
+ return restarted.kernel;
189
+ },
190
+ () => this.#registrations.unregister(language),
191
+ );
196
192
  if (generation !== this.#generation) {
197
- await kernel.close();
193
+ await resolvedKernel.close();
198
194
  throw new CodemodeSessionDisposedError();
199
195
  }
200
- this.#kernels.set(language, kernel);
201
- this.#registrations.set(
202
- language,
203
- registerKernel(this.#options.ownerSessionId ?? this.#options.sessionId, language, memory),
204
- );
196
+ this.#kernels.set(language, resolvedKernel);
197
+ this.#registrations.register(language, started.memory);
205
198
  // The directory can vanish while the interpreter starts; every caller sharing this creation
206
199
  // must see that, not only the next one. The kernel stays stored and dispose still closes it.
207
200
  await assertSessionCwdAvailable(this.#options.cwd);
208
201
  // A dispose that started during the check above already owns this stored kernel.
209
202
  if (generation !== this.#generation) throw new CodemodeSessionDisposedError();
210
- return kernel;
203
+ return resolvedKernel;
204
+ }
205
+
206
+ // py/rb/jl instances can die; the session holds one replaceable kernel per language so every
207
+ // cell that kept a reference to it survives the death (JS heals its own worker).
208
+ async #startLanguageKernel(
209
+ language: EvalLanguage,
210
+ onMessage: (message: KernelToHostMessage) => void,
211
+ ): Promise<{ readonly kernel: EvalKernel; readonly memory: RegisteredKernelSource | undefined }> {
212
+ if (language === "js") return await this.#createKernel(language, onMessage);
213
+ let memory: RegisteredKernelSource | undefined;
214
+ const kernel = await ReplaceableKernel.create(language, async (lifecycle) => {
215
+ const created = await this.#createKernel(language, onMessage, lifecycle);
216
+ memory ??= created.memory;
217
+ return created.kernel;
218
+ });
219
+ return { kernel, memory };
211
220
  }
212
221
 
213
222
  async #createKernel(
214
223
  language: EvalLanguage,
215
224
  onMessage: (message: KernelToHostMessage) => void,
225
+ lifecycle: KernelLifecycle = {},
216
226
  ): Promise<StartedKernel> {
217
227
  const bridge = this.#bridge;
218
228
  if (!bridge) throw new Error("codemode bridge server is not running");
@@ -255,6 +265,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
255
265
  ...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
256
266
  connection,
257
267
  onMessage,
268
+ ...lifecycle,
258
269
  };
259
270
  const memory = resolveKernelMemoryThresholds(this.#options.settings.memory);
260
271
  return await startSubprocessKernel({ language, interpreterPath: detected.path, memory, shared });
@@ -1,10 +1,12 @@
1
1
  import { join } from "node:path";
2
2
  import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
3
3
  import type { SessionEnvironment } from "../session-env.ts";
4
+ import type { KernelLifecycle } from "../shared/kernel-death.ts";
5
+ import { readProcessGroupCpuTime } from "../shared/process-group-cpu.ts";
4
6
  import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
5
7
  import { SubprocessKernel, type SubprocessKernelMemory, type SubprocessSpawn } from "../shared/subprocess-kernel.ts";
6
8
 
7
- export interface JuliaKernelStartOptions {
9
+ export interface JuliaKernelStartOptions extends KernelLifecycle {
8
10
  readonly cwd: string;
9
11
  readonly sessionId: string;
10
12
  readonly connection: BridgeConnectionConfig;
@@ -13,7 +15,7 @@ export interface JuliaKernelStartOptions {
13
15
  readonly command?: string;
14
16
  readonly spawn?: SubprocessSpawn;
15
17
  readonly onMessage?: (message: KernelToHostMessage) => void;
16
- /** Ceiling-only memory management (no notice or globals list: the runner reports no memory). */
18
+ /** Memory management: the runner reports its largest globals and the host reads the interpreter footprint for the ceiling. */
17
19
  readonly memory?: SubprocessKernelMemory;
18
20
  }
19
21
 
@@ -52,6 +54,8 @@ export class JuliaKernel extends SubprocessKernel {
52
54
  spawn: options.spawn,
53
55
  onMessage: options.onMessage,
54
56
  memory: options.memory && { language: "jl", ...options.memory },
57
+ onDeath: options.onDeath,
58
+ startup: { label: "Julia", readGroupCpuTime: readProcessGroupCpuTime },
55
59
  });
56
60
  }
57
61
  }
@@ -1,4 +1,6 @@
1
1
  # allow: SIZE_OK — parser, stream capture, bridge calls, and the persistent execution loop share Main globals.
2
+ write(stdout, "{\"type\":\"status\",\"event\":{\"op\":\"kernel-startup\",\"stage\":\"stdlib-imports\"}}\n")
3
+ flush(stdout)
2
4
  using Sockets
3
5
 
4
6
  const SENPI_ORIGINAL_STDOUT = stdout
@@ -7,6 +9,8 @@ out_read, out_write = redirect_stdout()
7
9
  err_read, err_write = redirect_stderr()
8
10
  redirect_stdin(devnull)
9
11
 
12
+ write(SENPI_ORIGINAL_STDOUT, "{\"type\":\"status\",\"event\":{\"op\":\"kernel-startup\",\"stage\":\"runtime-init\"}}\n")
13
+ flush(SENPI_ORIGINAL_STDOUT)
10
14
  include("prelude.jl")
11
15
 
12
16
  const senpi_connection = Dict{String, Any}()
@@ -281,6 +285,152 @@ function senpi_set_connection(value)
281
285
  end
282
286
  end
283
287
 
288
+ const senpi_memory = Ref{Any}(nothing)
289
+
290
+ const SENPI_MEMORY_INTERNALS = Set([:senpi_current_cell, :senpi_memory, :senpi_connection, :senpi_frame_io, :senpi_stdout_capture, :senpi_stderr_capture, :senpi_protocol_stdin])
291
+
292
+ const SENPI_SIZER_SAMPLE = 1_000
293
+ const SENPI_SIZER_NODE_BUDGET = 5_000
294
+ const SENPI_SIZER_MAX_DEPTH = 64
295
+ const SENPI_SIZER_POINTER = 8
296
+ const SENPI_SIZER_OBJECT = 16
297
+ const SENPI_SIZER_MIN_REPORTED = 1024 * 1024
298
+
299
+ # Sizes a global with sampling and a node budget per global (leaves count too), so one deep global
300
+ # never hides the ones measured after it and a huge container costs a bounded walk. Only concrete Base
301
+ # containers are iterated; any other AbstractDict or AbstractSet is sized as an opaque struct, so no user
302
+ # length or iterate method runs during a memory report.
303
+ mutable struct SenpiSizer
304
+ seen::IdDict{Any, Nothing}
305
+ nodes::Int
306
+ approximate::Bool
307
+ end
308
+ SenpiSizer() = SenpiSizer(IdDict{Any, Nothing}(), 0, false)
309
+
310
+ senpi_over_budget(sizer::SenpiSizer) = sizer.nodes >= SENPI_SIZER_NODE_BUDGET
311
+
312
+ function senpi_size(sizer::SenpiSizer, value, depth::Int)::Int
313
+ sizer.nodes += 1
314
+ value isa Type && return 0
315
+ value isa Union{Number, Char, Bool, Symbol, Nothing} && return isbits(value) ? sizeof(value) : 0
316
+ value isa String && return SENPI_SIZER_OBJECT + sizeof(value)
317
+ if ismutable(value)
318
+ haskey(sizer.seen, value) && return 0
319
+ sizer.seen[value] = nothing
320
+ end
321
+ if depth >= SENPI_SIZER_MAX_DEPTH || senpi_over_budget(sizer)
322
+ sizer.approximate = true
323
+ return SENPI_SIZER_OBJECT
324
+ end
325
+ if value isa Array
326
+ isbitstype(eltype(value)) && return SENPI_SIZER_OBJECT + sizeof(value)
327
+ return SENPI_SIZER_OBJECT + length(value) * SENPI_SIZER_POINTER +
328
+ senpi_sampled(sizer, length(value), depth) do index
329
+ isassigned(value, index) ? value[index] : nothing
330
+ end
331
+ elseif value isa Union{Dict, IdDict}
332
+ count = length(value)
333
+ total = 0
334
+ taken = 0
335
+ for (key, item) in value
336
+ senpi_over_budget(sizer) && break
337
+ total += senpi_size(sizer, key, depth + 1) + senpi_size(sizer, item, depth + 1)
338
+ taken += 1
339
+ taken >= SENPI_SIZER_SAMPLE && break
340
+ end
341
+ taken < count && (sizer.approximate = true)
342
+ return SENPI_SIZER_OBJECT + count * 2 * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
343
+ elseif value isa Union{Set, Tuple}
344
+ count = length(value)
345
+ total = 0
346
+ taken = 0
347
+ for item in value
348
+ senpi_over_budget(sizer) && break
349
+ total += senpi_size(sizer, item, depth + 1)
350
+ taken += 1
351
+ taken >= SENPI_SIZER_SAMPLE && break
352
+ end
353
+ taken < count && (sizer.approximate = true)
354
+ return SENPI_SIZER_OBJECT + count * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
355
+ end
356
+ isbits(value) && return sizeof(value)
357
+ fields = fieldcount(typeof(value))
358
+ return SENPI_SIZER_OBJECT + fields * SENPI_SIZER_POINTER +
359
+ senpi_sampled(sizer, fields, depth) do index
360
+ isdefined(value, index) ? getfield(value, index) : nothing
361
+ end
362
+ end
363
+
364
+ # Up to SENPI_SIZER_SAMPLE evenly spaced elements; once the budget runs out part-way, the elements measured
365
+ # so far stand in for the rest.
366
+ function senpi_sampled(at, sizer::SenpiSizer, count::Int, depth::Int)::Int
367
+ count == 0 && return 0
368
+ picks = min(count, SENPI_SIZER_SAMPLE)
369
+ picks < count && (sizer.approximate = true)
370
+ step = count / picks
371
+ total = 0
372
+ measured = 0
373
+ for sample in 1:picks
374
+ if senpi_over_budget(sizer)
375
+ sizer.approximate = true
376
+ break
377
+ end
378
+ total += senpi_size(sizer, at(1 + floor(Int, (sample - 1) * step)), depth + 1)
379
+ measured += 1
380
+ end
381
+ return measured == 0 ? 0 : round(Int, total / measured * count)
382
+ end
383
+
384
+ function senpi_largest_globals(limit::Int)
385
+ try
386
+ sizer = SenpiSizer()
387
+ candidates = Tuple{String, Int, Bool}[]
388
+ Base.invokelatest() do
389
+ for name in names(Main, all = true)
390
+ name in SENPI_MEMORY_INTERNALS && continue
391
+ name in (:Main, :Base, :Core, :Ans, :ans) && continue
392
+ lowered = lowercase(string(name))
393
+ startswith(lowered, "senpi_") && continue
394
+ startswith(string(name), "Senpi") && continue
395
+ startswith(string(name), "#") && continue
396
+ isdefined(Main, name) || continue
397
+ value = getfield(Main, name)
398
+ value isa Module && continue
399
+ value isa IO && continue
400
+ value isa Type && continue
401
+ value isa Function && continue
402
+ sizer.approximate = false
403
+ sizer.nodes = 0
404
+ bytes = try
405
+ senpi_size(sizer, value, 0) + SENPI_SIZER_POINTER
406
+ catch
407
+ 0
408
+ end
409
+ bytes >= SENPI_SIZER_MIN_REPORTED && push!(candidates, (string(name), bytes, sizer.approximate))
410
+ end
411
+ end
412
+ sort!(candidates, by = entry -> entry[2], rev = true)
413
+ [approximate ? Dict{String, Any}("name" => name, "bytes" => bytes, "approximate" => true) : Dict{String, Any}("name" => name, "bytes" => bytes) for (name, bytes, approximate) in candidates[1:min(limit, end)]]
414
+ catch
415
+ Dict{String, Any}[]
416
+ end
417
+ end
418
+
419
+ function senpi_memory_report()
420
+ try
421
+ senpi_memory[] === nothing && return nothing
422
+ # The host reads the interpreter's current footprint and decides on the notice; the runner only
423
+ # names its largest globals, with a bounded walk, so a cell never pays for an unbounded scan.
424
+ named = senpi_largest_globals(5)
425
+ # The host replaces liveBytes with the interpreter footprint it reads itself; only globals is used.
426
+ report = Dict{String, Any}("liveBytes" => 0, "measure" => "footprint")
427
+ isempty(named) || (report["globals"] = named)
428
+ report
429
+ catch
430
+ nothing
431
+ end
432
+ end
433
+
284
434
  function senpi_run_cell(message)
285
435
  cell_id = string(get(message, "cellId", ""))
286
436
  code = string(get(message, "code", ""))
@@ -297,6 +447,8 @@ function senpi_run_cell(message)
297
447
  yield()
298
448
  frame = Dict{String, Any}("type" => "result", "cellId" => cell_id, "ok" => true, "durationMs" => round(Int, (time() - started) * 1000))
299
449
  value !== nothing && senpi_should_display_result(parsed) && (frame["valueRepr"] = senpi_json(value))
450
+ memory = senpi_memory_report()
451
+ memory !== nothing && (frame["memory"] = memory)
300
452
  senpi_emit(frame)
301
453
  catch error
302
454
  senpi_emit(Dict("type" => "result", "cellId" => cell_id, "ok" => false, "error" => senpi_error(error), "durationMs" => round(Int, (time() - started) * 1000)))
@@ -313,7 +465,9 @@ while !eof(SENPI_ORIGINAL_STDIN)
313
465
  message isa AbstractDict || error("Bridge frame must be an object")
314
466
  kind = get(message, "type", nothing)
315
467
  if kind == "init"
468
+ senpi_emit(Dict("type" => "status", "event" => Dict("op" => "kernel-startup", "stage" => "host-init")))
316
469
  senpi_set_connection(get(message, "connection", nothing))
470
+ senpi_memory[] = get(message, "memory", nothing)
317
471
  senpi_emit(Dict("type" => "ready"))
318
472
  elseif kind == "run"
319
473
  senpi_run_cell(message)
@@ -1,6 +1,7 @@
1
1
  import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
2
2
  import { CHILD_LIFECYCLE_OP, INTERRUPT_ACK_OP } from "../../bridge/reserved.ts";
3
3
  import type { KernelInterruptHandle } from "../../tool/types.ts";
4
+ import { KernelToolHostPump } from "../shared/kernel-tools-pump.ts";
4
5
  import { ActiveCellControl } from "./active-cell-control.ts";
5
6
  import { DEFAULT_INTERRUPT_BOUNDS, type WorkerRetirement } from "./interrupt-bounds.ts";
6
7
  import {
@@ -14,7 +15,6 @@ import {
14
15
  } from "./kernel-contract.ts";
15
16
  import { type JavaScriptMemoryReading, KernelMemoryBridge } from "./kernel-memory-bridge.ts";
16
17
  import { kernelToolError } from "./kernel-tools-errors.ts";
17
- import { KernelToolHostPump } from "./kernel-tools-host.ts";
18
18
  import type {
19
19
  KernelToolsDescribeResult,
20
20
  KernelToolsInvokeOptions,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionKernelTools, KernelToolInvokeOptions, KernelToolInvokeScope } from "@code-yeongyu/senpi";
2
+ import type { EvalLanguage } from "../../tool/types.ts";
2
3
  import type { KernelToolErrorCode, KernelToolHostDenial, KernelToolHostDenialReason } from "./kernel-tools-errors.ts";
3
4
 
4
5
  export type { KernelToolErrorCode, KernelToolHostDenial, KernelToolHostDenialReason };
@@ -7,7 +8,7 @@ export type KernelToolDescriptor = {
7
8
  readonly name: string;
8
9
  readonly description: string;
9
10
  readonly input_schema: unknown;
10
- readonly language: "js";
11
+ readonly language: EvalLanguage;
11
12
  readonly kernel_generation: number;
12
13
  readonly definition_revision: number;
13
14
  };
@@ -24,6 +24,7 @@ const SESSION_ENVIRONMENT_KEYS = [
24
24
  "PI_PROVIDER",
25
25
  "PI_MODEL",
26
26
  "PI_REASONING_LEVEL",
27
+ "OMO_BROWSER_ENGINE",
27
28
  ];
28
29
 
29
30
  export function createWorkerCore(transport, options) {
@@ -9,6 +9,54 @@ const FUNCTION_BYTES = 64;
9
9
  const INTERNAL_GLOBAL_PREFIX = "__senpi";
10
10
  const MIN_REPORTED_BYTES = 1024 * 1024;
11
11
 
12
+ function intrinsicGetter(prototype, name) {
13
+ return Object.getOwnPropertyDescriptor(prototype, name)?.get;
14
+ }
15
+
16
+ // Built-in getters captured at load: a user subclass that overrides `byteLength` or `size` is never called.
17
+ const TYPED_ARRAY_BYTE_LENGTH = intrinsicGetter(Object.getPrototypeOf(Uint8Array.prototype), "byteLength");
18
+ const DATA_VIEW_BYTE_LENGTH = intrinsicGetter(DataView.prototype, "byteLength");
19
+ const ARRAY_BUFFER_BYTE_LENGTH = intrinsicGetter(ArrayBuffer.prototype, "byteLength");
20
+ const SHARED_ARRAY_BUFFER_BYTE_LENGTH =
21
+ typeof SharedArrayBuffer === "function" ? intrinsicGetter(SharedArrayBuffer.prototype, "byteLength") : undefined;
22
+ const MAP_SIZE = intrinsicGetter(Map.prototype, "size");
23
+ const SET_SIZE = intrinsicGetter(Set.prototype, "size");
24
+ const BLOB_SIZE = typeof Blob === "function" ? intrinsicGetter(Blob.prototype, "size") : undefined;
25
+ // Iteration captured at load too: a user who replaces Map.prototype.entries or an iterator's next() is never called.
26
+ const MAP_ENTRIES = Map.prototype.entries;
27
+ const SET_VALUES = Set.prototype.values;
28
+ const MAP_ITERATOR_NEXT = Object.getPrototypeOf(new Map().entries()).next;
29
+ const SET_ITERATOR_NEXT = Object.getPrototypeOf(new Set().values()).next;
30
+
31
+ function read(getter, value) {
32
+ return Reflect.apply(getter, value, []);
33
+ }
34
+
35
+ const GET_PROTOTYPE_OF = Object.getPrototypeOf;
36
+ const BLOB_PROTOTYPE = typeof Blob === "function" ? Blob.prototype : undefined;
37
+
38
+ function inheritsFromBlob(value) {
39
+ for (let prototype = GET_PROTOTYPE_OF(value); prototype !== null; prototype = GET_PROTOTYPE_OF(prototype)) {
40
+ // A Proxy in the chain would run its getPrototypeOf trap on the next step.
41
+ if (types.isProxy(prototype)) return false;
42
+ if (prototype === BLOB_PROTOTYPE) return true;
43
+ }
44
+ return false;
45
+ }
46
+
47
+ // The Blob brand: a cheap prototype-chain pre-check (no user hook; proxies never reach here) so ordinary
48
+ // objects skip the try, then the size getter as the proof, which throws on a forged look-alike.
49
+ // instanceof would call a user-redefinable Symbol.hasInstance.
50
+ function isBlob(value) {
51
+ if (BLOB_SIZE === undefined || !inheritsFromBlob(value)) return false;
52
+ try {
53
+ read(BLOB_SIZE, value);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
12
60
  export function captureGlobalBaseline() {
13
61
  return new Set(Object.getOwnPropertyNames(globalThis));
14
62
  }
@@ -55,11 +103,12 @@ function createSizer() {
55
103
  return (total / SAMPLED_ELEMENTS) * length;
56
104
  }
57
105
 
58
- function entriesOf(iterable, count, depth, sizeOf) {
106
+ function entriesOf(iterator, next, count, depth, sizeOf) {
59
107
  const taken = [];
60
- for (const entry of iterable) {
61
- taken.push(entry);
62
- if (taken.length >= SAMPLED_ELEMENTS) break;
108
+ while (taken.length < SAMPLED_ELEMENTS) {
109
+ const step = Reflect.apply(next, iterator, []);
110
+ if (step.done) break;
111
+ taken.push(step.value);
63
112
  }
64
113
  if (taken.length < count) approximate = true;
65
114
  let total = 0;
@@ -67,21 +116,42 @@ function createSizer() {
67
116
  return total * (count / Math.max(1, taken.length));
68
117
  }
69
118
 
119
+ // An own data element, never a getter: an accessor is left unsized and makes the estimate approximate.
120
+ function elementOf(array, index) {
121
+ const descriptor = Object.getOwnPropertyDescriptor(array, index);
122
+ if (descriptor === undefined) return undefined;
123
+ if ("value" in descriptor) return descriptor.value;
124
+ approximate = true;
125
+ return undefined;
126
+ }
127
+
70
128
  function objectSize(value, depth) {
71
129
  if (types.isProxy(value)) return OBJECT_BYTES;
72
- if (ArrayBuffer.isView(value)) return value.byteLength;
73
- if (types.isAnyArrayBuffer(value)) return value.byteLength;
74
- if (typeof Blob === "function" && value instanceof Blob) return value.size;
75
- if (Array.isArray(value)) return OBJECT_BYTES + value.length * POINTER_BYTES + sampled(value.length, (index) => value[index], depth);
76
- if (value instanceof Map) {
77
- const entries = Map.prototype.entries.call(value);
78
- return OBJECT_BYTES + value.size * 2 * POINTER_BYTES + entriesOf(entries, value.size, depth, ([key, item], at) => size(key, at) + size(item, at));
130
+ if (types.isTypedArray(value)) return read(TYPED_ARRAY_BYTE_LENGTH, value);
131
+ if (types.isDataView(value)) return read(DATA_VIEW_BYTE_LENGTH, value);
132
+ if (types.isArrayBuffer(value)) return read(ARRAY_BUFFER_BYTE_LENGTH, value);
133
+ if (types.isSharedArrayBuffer(value) && SHARED_ARRAY_BUFFER_BYTE_LENGTH) return read(SHARED_ARRAY_BUFFER_BYTE_LENGTH, value);
134
+ if (isBlob(value)) return read(BLOB_SIZE, value);
135
+ if (Array.isArray(value)) {
136
+ const length = value.length;
137
+ return OBJECT_BYTES + length * POINTER_BYTES + sampled(length, (index) => elementOf(value, index), depth);
138
+ }
139
+ if (types.isMap(value)) {
140
+ const count = read(MAP_SIZE, value);
141
+ const entries = Reflect.apply(MAP_ENTRIES, value, []);
142
+ return OBJECT_BYTES + count * 2 * POINTER_BYTES + entriesOf(entries, MAP_ITERATOR_NEXT, count, depth, ([key, item], at) => size(key, at) + size(item, at));
143
+ }
144
+ if (types.isSet(value)) {
145
+ const count = read(SET_SIZE, value);
146
+ return OBJECT_BYTES + count * POINTER_BYTES + entriesOf(Reflect.apply(SET_VALUES, value, []), SET_ITERATOR_NEXT, count, depth, size);
79
147
  }
80
- if (value instanceof Set) return OBJECT_BYTES + value.size * POINTER_BYTES + entriesOf(Set.prototype.values.call(value), value.size, depth, size);
81
148
  const keys = Object.keys(value);
82
149
  const propertyValue = (index) => {
83
150
  const descriptor = Object.getOwnPropertyDescriptor(value, keys[index]);
84
- return descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined;
151
+ if (descriptor === undefined) return undefined;
152
+ if ("value" in descriptor) return descriptor.value;
153
+ approximate = true;
154
+ return undefined;
85
155
  };
86
156
  return OBJECT_BYTES + keys.length * POINTER_BYTES + sampled(keys.length, propertyValue, depth);
87
157
  }
@@ -114,7 +184,7 @@ function createSizer() {
114
184
  try {
115
185
  return { bytes: POINTER_BYTES + size(value, 0), approximate };
116
186
  } catch (error) {
117
- // A user value can still throw from an exotic element read (an index getter); it is left unsized.
187
+ // A forged built-in look-alike (a Blob-prototype object without its slot) throws from the intrinsic getter; it is left unsized.
118
188
  if (error instanceof Error) return undefined;
119
189
  throw error;
120
190
  }
@@ -2,11 +2,12 @@ import type { KernelMemoryThresholds } from "../../bridge/memory-protocol.ts";
2
2
  import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
3
3
  import type { EvalKernelRunInput } from "../../tool/types.ts";
4
4
  import type { SessionEnvironment } from "../session-env.ts";
5
+ import type { KernelLifecycle } from "../shared/kernel-death.ts";
5
6
  import type { KernelSpawnProcess } from "./process.ts";
6
7
  import type { PythonStartupStage } from "./startup.ts";
7
8
  import type { PythonTransportResult } from "./transport.ts";
8
9
 
9
- export interface PythonKernelStartOptions {
10
+ export interface PythonKernelStartOptions extends KernelLifecycle {
10
11
  readonly interpreterPath: string;
11
12
  readonly sessionId: string;
12
13
  readonly cwd: string;