@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.
- package/CHANGELOG.md +37 -0
- package/README.md +27 -2
- package/package.json +4 -4
- package/src/bridge/kernel-tools-protocol.ts +1 -1
- package/src/bridge/protocol.ts +11 -0
- package/src/config/feature-settings.ts +110 -0
- package/src/config/memory-settings.ts +8 -0
- package/src/config/settings-constants.ts +7 -0
- package/src/config/settings-overrides.ts +72 -0
- package/src/config/settings.ts +35 -78
- package/src/config/unknown-settings.ts +19 -0
- package/src/extension/bridge-tool-call.ts +37 -0
- package/src/extension/idle-parking-kernel.ts +272 -0
- package/src/extension/kernel-registration.ts +25 -0
- package/src/extension/kernel-replacement.ts +230 -0
- package/src/extension/kernel-tools-probe.ts +24 -0
- package/src/extension/session-manager.ts +49 -38
- package/src/kernels/jl/kernel.ts +6 -2
- package/src/kernels/jl/runner.jl +154 -0
- package/src/kernels/js/context-manager.ts +1 -1
- package/src/kernels/js/kernel-tools-types.ts +2 -1
- package/src/kernels/js/worker-core.js +1 -0
- package/src/kernels/js/worker-global-sizes.js +84 -14
- package/src/kernels/py/kernel-contract.ts +2 -1
- package/src/kernels/py/kernel.ts +48 -8
- package/src/kernels/py/prelude.py +6 -1
- package/src/kernels/py/transport.ts +25 -4
- package/src/kernels/rb/kernel.ts +6 -2
- package/src/kernels/rb/runner.rb +141 -0
- package/src/kernels/session-env.ts +3 -0
- package/src/kernels/shared/kernel-death.ts +39 -0
- package/src/kernels/shared/kernel-memory-host.ts +14 -6
- package/src/kernels/{js/kernel-tools-host.ts → shared/kernel-tools-pump.ts} +2 -2
- package/src/kernels/shared/process-group-cpu.ts +139 -0
- package/src/kernels/shared/subprocess-contract.ts +5 -1
- package/src/kernels/shared/subprocess-kernel.ts +66 -3
- package/src/kernels/shared/subprocess-queue.ts +5 -0
- package/src/kernels/shared/subprocess-startup.ts +90 -0
- package/src/tool/cell-runtime.ts +17 -4
- package/src/tool/detached-cell-notification.ts +13 -2
- package/src/tool/run-eval-cell.ts +5 -6
- 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 {
|
|
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
|
-
|
|
15
|
+
SessionKernelRegistrations,
|
|
16
16
|
type StartedKernel,
|
|
17
17
|
startSubprocessKernel,
|
|
18
18
|
} from "./kernel-registration.ts";
|
|
19
|
-
import {
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
193
|
+
await resolvedKernel.close();
|
|
198
194
|
throw new CodemodeSessionDisposedError();
|
|
199
195
|
}
|
|
200
|
-
this.#kernels.set(language,
|
|
201
|
-
this.#registrations.
|
|
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
|
|
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 });
|
package/src/kernels/jl/kernel.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
}
|
package/src/kernels/jl/runner.jl
CHANGED
|
@@ -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:
|
|
11
|
+
readonly language: EvalLanguage;
|
|
11
12
|
readonly kernel_generation: number;
|
|
12
13
|
readonly definition_revision: number;
|
|
13
14
|
};
|
|
@@ -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(
|
|
106
|
+
function entriesOf(iterator, next, count, depth, sizeOf) {
|
|
59
107
|
const taken = [];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
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 (
|
|
73
|
-
if (types.
|
|
74
|
-
if (
|
|
75
|
-
if (
|
|
76
|
-
if (value
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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;
|