@code-yeongyu/senpi-codemode 2026.10.10 → 2026.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 +196 -0
- package/README.md +113 -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/packages-install.ts +98 -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/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 +42 -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 +2 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/globals.jl +128 -0
- package/src/kernels/jl/prelude.jl +15 -2
- package/src/kernels/jl/runner.jl +35 -143
- 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 +23 -24
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/group-signal-notice.js +117 -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 +8 -0
- 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 +51 -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-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/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 +26 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
- package/src/kernels/shared/bun-ffi.ts +24 -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 +3 -0
- 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-tool.ts +4 -2
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- 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 +24 -14
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +13 -2
|
@@ -20,6 +20,8 @@ export interface InterpreterDetected {
|
|
|
20
20
|
|
|
21
21
|
export interface InterpreterUnavailable {
|
|
22
22
|
readonly ok: false;
|
|
23
|
+
/** Why a configured interpreter could not be used; unset when detection simply found none. */
|
|
24
|
+
readonly reason?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export type InterpreterDetection = InterpreterDetected | InterpreterUnavailable;
|
|
@@ -43,6 +45,8 @@ export interface CreateInterpreterDetectorOptions {
|
|
|
43
45
|
|
|
44
46
|
export interface InterpreterDetector {
|
|
45
47
|
detect(language: CodemodeLanguage): Promise<InterpreterDetection>;
|
|
48
|
+
/** Probes exactly this executable (no PATH candidates, no splitting on spaces). */
|
|
49
|
+
detectExplicit(path: string): Promise<InterpreterDetection>;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
export interface LanguageAvailability {
|
|
@@ -74,6 +78,9 @@ export function createInterpreterDetector(options: CreateInterpreterDetectorOpti
|
|
|
74
78
|
cache.set(language, pending);
|
|
75
79
|
return pending;
|
|
76
80
|
},
|
|
81
|
+
detectExplicit(path) {
|
|
82
|
+
return probeExplicit(path, probe);
|
|
83
|
+
},
|
|
77
84
|
};
|
|
78
85
|
}
|
|
79
86
|
|
|
@@ -82,7 +89,7 @@ export async function getInterpreterAvailability(
|
|
|
82
89
|
detector: InterpreterDetector,
|
|
83
90
|
): Promise<InterpreterAvailability> {
|
|
84
91
|
return {
|
|
85
|
-
py: await
|
|
92
|
+
py: await pythonAvailability(settings, detector),
|
|
86
93
|
js: await availabilityFor("js", settings.languages.js, detector),
|
|
87
94
|
rb: await availabilityFor("rb", settings.languages.rb, detector),
|
|
88
95
|
jl: await availabilityFor("jl", settings.languages.jl, detector),
|
|
@@ -100,6 +107,32 @@ async function availabilityFor(
|
|
|
100
107
|
};
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
/** `languages.pyInterpreter`, when set, is the only interpreter Python uses; a path that does not answer makes Python unavailable. */
|
|
111
|
+
async function pythonAvailability(
|
|
112
|
+
settings: CodemodeSettings,
|
|
113
|
+
detector: InterpreterDetector,
|
|
114
|
+
): Promise<LanguageAvailability> {
|
|
115
|
+
const configured = settings.languages.pyInterpreter;
|
|
116
|
+
if (configured === undefined || !settings.languages.py)
|
|
117
|
+
return await availabilityFor("py", settings.languages.py, detector);
|
|
118
|
+
return { enabled: true, detected: await detector.detectExplicit(configured) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function probeExplicit(path: string, probe: ExecFileProbe): Promise<InterpreterDetection> {
|
|
122
|
+
const reason = (detail: string): InterpreterUnavailable => ({
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: `languages.pyInterpreter "${path}" ${detail}; Python is unavailable in this session`,
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
const result = await probe(path, ["--version"], { timeoutMs: probeTimeoutMs });
|
|
128
|
+
const version = parseVersion(`${result.stdout}\n${result.stderr}`);
|
|
129
|
+
if (version === null) return reason("did not report a Python version");
|
|
130
|
+
return { ok: true, path, version, resolvedPath: path };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return reason(`could not run (${error instanceof Error ? error.message.split("\n")[0] : String(error)})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
103
136
|
async function detectUncached(
|
|
104
137
|
language: CodemodeLanguage,
|
|
105
138
|
hostPlatform: NodeJS.Platform,
|
package/src/kernels/AGENTS.md
CHANGED
|
@@ -16,6 +16,8 @@ runner/prelude assets).
|
|
|
16
16
|
| Julia kernel | `jl/kernel.ts` + `jl/prelude.jl`, `jl/runner.jl` |
|
|
17
17
|
| Shared subprocess layer | `shared/subprocess-kernel.ts`, `subprocess-{contract,process,queue,run}.ts`, `runtime-asset.ts` |
|
|
18
18
|
| Session environment | `session-env.ts` (PI_* contract shared by all kernels; mirrors the core bash tool) |
|
|
19
|
+
| JS process isolation | `js/process-worker.ts` (host side), `js/process-entry.js` (child entry), `js/kernel-memory.ts`, `js/control-frames.ts` |
|
|
20
|
+
| Sandbox cells | `sandbox/sandbox-cell.ts` (host executor); `sandbox/vendor/pi-codemode/**` (vendored runtime, see its `VENDORED.md`) |
|
|
19
21
|
|
|
20
22
|
## CONVENTIONS
|
|
21
23
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const SENPI_MEMORY_INTERNALS = Set([:senpi_current_cell, :senpi_connection, :senpi_frame_io, :senpi_stdout_capture, :senpi_stderr_capture, :senpi_protocol_stdin])
|
|
2
|
+
|
|
3
|
+
const SENPI_SIZER_SAMPLE = 1_000
|
|
4
|
+
const SENPI_SIZER_NODE_BUDGET = 5_000
|
|
5
|
+
const SENPI_SIZER_MAX_DEPTH = 64
|
|
6
|
+
const SENPI_SIZER_POINTER = 8
|
|
7
|
+
const SENPI_SIZER_OBJECT = 16
|
|
8
|
+
const SENPI_SIZER_MIN_REPORTED = 1024 * 1024
|
|
9
|
+
|
|
10
|
+
# Sizes a global with sampling and a node budget per global (leaves count too), so one deep global
|
|
11
|
+
# never hides the ones measured after it and a huge container costs a bounded walk. Only concrete Base
|
|
12
|
+
# containers are iterated; any other AbstractDict or AbstractSet is sized as an opaque struct, so no user
|
|
13
|
+
# length or iterate method runs during a memory report.
|
|
14
|
+
mutable struct SenpiSizer
|
|
15
|
+
seen::IdDict{Any, Nothing}
|
|
16
|
+
nodes::Int
|
|
17
|
+
approximate::Bool
|
|
18
|
+
end
|
|
19
|
+
SenpiSizer() = SenpiSizer(IdDict{Any, Nothing}(), 0, false)
|
|
20
|
+
|
|
21
|
+
senpi_over_budget(sizer::SenpiSizer) = sizer.nodes >= SENPI_SIZER_NODE_BUDGET
|
|
22
|
+
|
|
23
|
+
function senpi_size(sizer::SenpiSizer, value, depth::Int)::Int
|
|
24
|
+
sizer.nodes += 1
|
|
25
|
+
value isa Type && return 0
|
|
26
|
+
value isa Union{Number, Char, Bool, Symbol, Nothing} && return isbits(value) ? sizeof(value) : 0
|
|
27
|
+
value isa String && return SENPI_SIZER_OBJECT + sizeof(value)
|
|
28
|
+
if ismutable(value)
|
|
29
|
+
haskey(sizer.seen, value) && return 0
|
|
30
|
+
sizer.seen[value] = nothing
|
|
31
|
+
end
|
|
32
|
+
if depth >= SENPI_SIZER_MAX_DEPTH || senpi_over_budget(sizer)
|
|
33
|
+
sizer.approximate = true
|
|
34
|
+
return SENPI_SIZER_OBJECT
|
|
35
|
+
end
|
|
36
|
+
if value isa Array
|
|
37
|
+
isbitstype(eltype(value)) && return SENPI_SIZER_OBJECT + sizeof(value)
|
|
38
|
+
return SENPI_SIZER_OBJECT + length(value) * SENPI_SIZER_POINTER +
|
|
39
|
+
senpi_sampled(sizer, length(value), depth) do index
|
|
40
|
+
isassigned(value, index) ? value[index] : nothing
|
|
41
|
+
end
|
|
42
|
+
elseif value isa Union{Dict, IdDict}
|
|
43
|
+
count = length(value)
|
|
44
|
+
total = 0
|
|
45
|
+
taken = 0
|
|
46
|
+
for (key, item) in value
|
|
47
|
+
senpi_over_budget(sizer) && break
|
|
48
|
+
total += senpi_size(sizer, key, depth + 1) + senpi_size(sizer, item, depth + 1)
|
|
49
|
+
taken += 1
|
|
50
|
+
taken >= SENPI_SIZER_SAMPLE && break
|
|
51
|
+
end
|
|
52
|
+
taken < count && (sizer.approximate = true)
|
|
53
|
+
return SENPI_SIZER_OBJECT + count * 2 * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
|
|
54
|
+
elseif value isa Union{Set, Tuple}
|
|
55
|
+
count = length(value)
|
|
56
|
+
total = 0
|
|
57
|
+
taken = 0
|
|
58
|
+
for item in value
|
|
59
|
+
senpi_over_budget(sizer) && break
|
|
60
|
+
total += senpi_size(sizer, item, depth + 1)
|
|
61
|
+
taken += 1
|
|
62
|
+
taken >= SENPI_SIZER_SAMPLE && break
|
|
63
|
+
end
|
|
64
|
+
taken < count && (sizer.approximate = true)
|
|
65
|
+
return SENPI_SIZER_OBJECT + count * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
|
|
66
|
+
end
|
|
67
|
+
isbits(value) && return sizeof(value)
|
|
68
|
+
fields = fieldcount(typeof(value))
|
|
69
|
+
return SENPI_SIZER_OBJECT + fields * SENPI_SIZER_POINTER +
|
|
70
|
+
senpi_sampled(sizer, fields, depth) do index
|
|
71
|
+
isdefined(value, index) ? getfield(value, index) : nothing
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Up to SENPI_SIZER_SAMPLE evenly spaced elements; once the budget runs out part-way, the elements measured
|
|
76
|
+
# so far stand in for the rest.
|
|
77
|
+
function senpi_sampled(at, sizer::SenpiSizer, count::Int, depth::Int)::Int
|
|
78
|
+
count == 0 && return 0
|
|
79
|
+
picks = min(count, SENPI_SIZER_SAMPLE)
|
|
80
|
+
picks < count && (sizer.approximate = true)
|
|
81
|
+
step = count / picks
|
|
82
|
+
total = 0
|
|
83
|
+
measured = 0
|
|
84
|
+
for sample in 1:picks
|
|
85
|
+
if senpi_over_budget(sizer)
|
|
86
|
+
sizer.approximate = true
|
|
87
|
+
break
|
|
88
|
+
end
|
|
89
|
+
total += senpi_size(sizer, at(1 + floor(Int, (sample - 1) * step)), depth + 1)
|
|
90
|
+
measured += 1
|
|
91
|
+
end
|
|
92
|
+
return measured == 0 ? 0 : round(Int, total / measured * count)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
function senpi_largest_globals(limit::Int)
|
|
96
|
+
try
|
|
97
|
+
sizer = SenpiSizer()
|
|
98
|
+
candidates = Tuple{String, Int, Bool}[]
|
|
99
|
+
Base.invokelatest() do
|
|
100
|
+
for name in names(Main, all = true)
|
|
101
|
+
name in SENPI_MEMORY_INTERNALS && continue
|
|
102
|
+
name in (:Main, :Base, :Core, :Ans, :ans) && continue
|
|
103
|
+
lowered = lowercase(string(name))
|
|
104
|
+
startswith(lowered, "senpi_") && continue
|
|
105
|
+
startswith(string(name), "Senpi") && continue
|
|
106
|
+
startswith(string(name), "#") && continue
|
|
107
|
+
isdefined(Main, name) || continue
|
|
108
|
+
value = getfield(Main, name)
|
|
109
|
+
value isa Module && continue
|
|
110
|
+
value isa IO && continue
|
|
111
|
+
value isa Type && continue
|
|
112
|
+
value isa Function && continue
|
|
113
|
+
sizer.approximate = false
|
|
114
|
+
sizer.nodes = 0
|
|
115
|
+
bytes = try
|
|
116
|
+
senpi_size(sizer, value, 0) + SENPI_SIZER_POINTER
|
|
117
|
+
catch
|
|
118
|
+
0
|
|
119
|
+
end
|
|
120
|
+
bytes >= SENPI_SIZER_MIN_REPORTED && push!(candidates, (string(name), bytes, sizer.approximate))
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
sort!(candidates, by = entry -> entry[2], rev = true)
|
|
124
|
+
[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)]]
|
|
125
|
+
catch
|
|
126
|
+
Dict{String, Any}[]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -231,12 +231,25 @@ function workpool(agent::AbstractDict, name::AbstractString; mode=nothing, tools
|
|
|
231
231
|
throw(SenpiBridgeError("Host did not return a workpool identity", "workpool_unavailable"))
|
|
232
232
|
end
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
const SENPI_HANDLES_INCLUDED = Ref(false)
|
|
235
|
+
const SENPI_HANDLES_INCLUDE_LOCK = ReentrantLock()
|
|
236
|
+
|
|
237
|
+
function senpi_ensure_handles()
|
|
238
|
+
lock(SENPI_HANDLES_INCLUDE_LOCK) do
|
|
239
|
+
if !SENPI_HANDLES_INCLUDED[]
|
|
240
|
+
include(joinpath(@__DIR__, "handles.jl"))
|
|
241
|
+
SENPI_HANDLES_INCLUDED[] = true
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
nothing
|
|
245
|
+
end
|
|
235
246
|
|
|
236
247
|
# Rich view of an agent record, workpool, completion handle or saved reference; see tool_schema("eval:helpers").
|
|
237
248
|
# The barrier is `Base.wait` on the view types (never a `Main.wait`): wait(handle(node)) or wait([handle(a), handle(b)]).
|
|
238
249
|
function handle(value)
|
|
239
|
-
|
|
250
|
+
senpi_ensure_handles()
|
|
251
|
+
# Julia 1.12 ages global bindings too: resolve the new helper inside the latest world.
|
|
252
|
+
Base.invokelatest(() -> senpi_handle_view(value))
|
|
240
253
|
end
|
|
241
254
|
|
|
242
255
|
function completion(prompt::AbstractString; model="default", system=nothing, schema=nothing, kwargs...)
|
package/src/kernels/jl/runner.jl
CHANGED
|
@@ -16,6 +16,7 @@ include("prelude.jl")
|
|
|
16
16
|
const senpi_connection = Dict{String, Any}()
|
|
17
17
|
const senpi_write_lock = ReentrantLock()
|
|
18
18
|
global senpi_current_cell = nothing
|
|
19
|
+
global senpi_memory_cell = nothing
|
|
19
20
|
|
|
20
21
|
function senpi_escape(text::AbstractString)
|
|
21
22
|
out = IOBuffer()
|
|
@@ -290,150 +291,31 @@ function senpi_set_connection(value)
|
|
|
290
291
|
end
|
|
291
292
|
end
|
|
292
293
|
|
|
293
|
-
const
|
|
294
|
+
const SENPI_GLOBALS_INCLUDED = Ref(false)
|
|
294
295
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const SENPI_SIZER_MIN_REPORTED = 1024 * 1024
|
|
303
|
-
|
|
304
|
-
# Sizes a global with sampling and a node budget per global (leaves count too), so one deep global
|
|
305
|
-
# never hides the ones measured after it and a huge container costs a bounded walk. Only concrete Base
|
|
306
|
-
# containers are iterated; any other AbstractDict or AbstractSet is sized as an opaque struct, so no user
|
|
307
|
-
# length or iterate method runs during a memory report.
|
|
308
|
-
mutable struct SenpiSizer
|
|
309
|
-
seen::IdDict{Any, Nothing}
|
|
310
|
-
nodes::Int
|
|
311
|
-
approximate::Bool
|
|
312
|
-
end
|
|
313
|
-
SenpiSizer() = SenpiSizer(IdDict{Any, Nothing}(), 0, false)
|
|
314
|
-
|
|
315
|
-
senpi_over_budget(sizer::SenpiSizer) = sizer.nodes >= SENPI_SIZER_NODE_BUDGET
|
|
316
|
-
|
|
317
|
-
function senpi_size(sizer::SenpiSizer, value, depth::Int)::Int
|
|
318
|
-
sizer.nodes += 1
|
|
319
|
-
value isa Type && return 0
|
|
320
|
-
value isa Union{Number, Char, Bool, Symbol, Nothing} && return isbits(value) ? sizeof(value) : 0
|
|
321
|
-
value isa String && return SENPI_SIZER_OBJECT + sizeof(value)
|
|
322
|
-
if ismutable(value)
|
|
323
|
-
haskey(sizer.seen, value) && return 0
|
|
324
|
-
sizer.seen[value] = nothing
|
|
325
|
-
end
|
|
326
|
-
if depth >= SENPI_SIZER_MAX_DEPTH || senpi_over_budget(sizer)
|
|
327
|
-
sizer.approximate = true
|
|
328
|
-
return SENPI_SIZER_OBJECT
|
|
329
|
-
end
|
|
330
|
-
if value isa Array
|
|
331
|
-
isbitstype(eltype(value)) && return SENPI_SIZER_OBJECT + sizeof(value)
|
|
332
|
-
return SENPI_SIZER_OBJECT + length(value) * SENPI_SIZER_POINTER +
|
|
333
|
-
senpi_sampled(sizer, length(value), depth) do index
|
|
334
|
-
isassigned(value, index) ? value[index] : nothing
|
|
335
|
-
end
|
|
336
|
-
elseif value isa Union{Dict, IdDict}
|
|
337
|
-
count = length(value)
|
|
338
|
-
total = 0
|
|
339
|
-
taken = 0
|
|
340
|
-
for (key, item) in value
|
|
341
|
-
senpi_over_budget(sizer) && break
|
|
342
|
-
total += senpi_size(sizer, key, depth + 1) + senpi_size(sizer, item, depth + 1)
|
|
343
|
-
taken += 1
|
|
344
|
-
taken >= SENPI_SIZER_SAMPLE && break
|
|
345
|
-
end
|
|
346
|
-
taken < count && (sizer.approximate = true)
|
|
347
|
-
return SENPI_SIZER_OBJECT + count * 2 * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
|
|
348
|
-
elseif value isa Union{Set, Tuple}
|
|
349
|
-
count = length(value)
|
|
350
|
-
total = 0
|
|
351
|
-
taken = 0
|
|
352
|
-
for item in value
|
|
353
|
-
senpi_over_budget(sizer) && break
|
|
354
|
-
total += senpi_size(sizer, item, depth + 1)
|
|
355
|
-
taken += 1
|
|
356
|
-
taken >= SENPI_SIZER_SAMPLE && break
|
|
357
|
-
end
|
|
358
|
-
taken < count && (sizer.approximate = true)
|
|
359
|
-
return SENPI_SIZER_OBJECT + count * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
|
|
360
|
-
end
|
|
361
|
-
isbits(value) && return sizeof(value)
|
|
362
|
-
fields = fieldcount(typeof(value))
|
|
363
|
-
return SENPI_SIZER_OBJECT + fields * SENPI_SIZER_POINTER +
|
|
364
|
-
senpi_sampled(sizer, fields, depth) do index
|
|
365
|
-
isdefined(value, index) ? getfield(value, index) : nothing
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
# Up to SENPI_SIZER_SAMPLE evenly spaced elements; once the budget runs out part-way, the elements measured
|
|
370
|
-
# so far stand in for the rest.
|
|
371
|
-
function senpi_sampled(at, sizer::SenpiSizer, count::Int, depth::Int)::Int
|
|
372
|
-
count == 0 && return 0
|
|
373
|
-
picks = min(count, SENPI_SIZER_SAMPLE)
|
|
374
|
-
picks < count && (sizer.approximate = true)
|
|
375
|
-
step = count / picks
|
|
376
|
-
total = 0
|
|
377
|
-
measured = 0
|
|
378
|
-
for sample in 1:picks
|
|
379
|
-
if senpi_over_budget(sizer)
|
|
380
|
-
sizer.approximate = true
|
|
381
|
-
break
|
|
296
|
+
function senpi_memory_globals(limit::Int)
|
|
297
|
+
if !SENPI_GLOBALS_INCLUDED[]
|
|
298
|
+
try
|
|
299
|
+
include(joinpath(@__DIR__, "globals.jl"))
|
|
300
|
+
SENPI_GLOBALS_INCLUDED[] = true
|
|
301
|
+
catch
|
|
302
|
+
return Dict{String, Any}[]
|
|
382
303
|
end
|
|
383
|
-
total += senpi_size(sizer, at(1 + floor(Int, (sample - 1) * step)), depth + 1)
|
|
384
|
-
measured += 1
|
|
385
304
|
end
|
|
386
|
-
|
|
305
|
+
Base.invokelatest(() -> senpi_largest_globals(limit))
|
|
387
306
|
end
|
|
388
307
|
|
|
389
|
-
function
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
name in (:Main, :Base, :Core, :Ans, :ans) && continue
|
|
397
|
-
lowered = lowercase(string(name))
|
|
398
|
-
startswith(lowered, "senpi_") && continue
|
|
399
|
-
startswith(string(name), "Senpi") && continue
|
|
400
|
-
startswith(string(name), "#") && continue
|
|
401
|
-
isdefined(Main, name) || continue
|
|
402
|
-
value = getfield(Main, name)
|
|
403
|
-
value isa Module && continue
|
|
404
|
-
value isa IO && continue
|
|
405
|
-
value isa Type && continue
|
|
406
|
-
value isa Function && continue
|
|
407
|
-
sizer.approximate = false
|
|
408
|
-
sizer.nodes = 0
|
|
409
|
-
bytes = try
|
|
410
|
-
senpi_size(sizer, value, 0) + SENPI_SIZER_POINTER
|
|
411
|
-
catch
|
|
412
|
-
0
|
|
413
|
-
end
|
|
414
|
-
bytes >= SENPI_SIZER_MIN_REPORTED && push!(candidates, (string(name), bytes, sizer.approximate))
|
|
415
|
-
end
|
|
416
|
-
end
|
|
417
|
-
sort!(candidates, by = entry -> entry[2], rev = true)
|
|
418
|
-
[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)]]
|
|
419
|
-
catch
|
|
420
|
-
Dict{String, Any}[]
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
function senpi_memory_report()
|
|
425
|
-
try
|
|
426
|
-
senpi_memory[] === nothing && return nothing
|
|
427
|
-
# The host reads the interpreter's current footprint and decides on the notice; the runner only
|
|
428
|
-
# names its largest globals, with a bounded walk, so a cell never pays for an unbounded scan.
|
|
429
|
-
named = senpi_largest_globals(5)
|
|
430
|
-
# The host replaces liveBytes with the interpreter footprint it reads itself; only globals is used.
|
|
431
|
-
report = Dict{String, Any}("liveBytes" => 0, "measure" => "footprint")
|
|
432
|
-
isempty(named) || (report["globals"] = named)
|
|
433
|
-
report
|
|
434
|
-
catch
|
|
435
|
-
nothing
|
|
308
|
+
function senpi_needs_handles(expression)
|
|
309
|
+
if expression isa Symbol
|
|
310
|
+
expression in (:handle, :completion, :eval, :include, :getfield, :getglobal, :deserialize) && return true
|
|
311
|
+
name = string(expression)
|
|
312
|
+
return startswith(name, "SenpiHandle") || startswith(name, "senpi_handle") ||
|
|
313
|
+
startswith(name, "senpi_wait") ||
|
|
314
|
+
(startswith(name, "SENPI_") && (occursin("HANDLE", name) || occursin("WAIT", name)))
|
|
436
315
|
end
|
|
316
|
+
expression isa QuoteNode && return senpi_needs_handles(expression.value)
|
|
317
|
+
expression isa Expr && return any(senpi_needs_handles, expression.args)
|
|
318
|
+
false
|
|
437
319
|
end
|
|
438
320
|
|
|
439
321
|
function senpi_run_cell(message)
|
|
@@ -446,19 +328,23 @@ function senpi_run_cell(message)
|
|
|
446
328
|
if parsed isa Expr && parsed.head === :error
|
|
447
329
|
error(string(parsed.args[1]))
|
|
448
330
|
end
|
|
331
|
+
# Install bindings before user functions/type annotations capture their world.
|
|
332
|
+
!SENPI_HANDLES_INCLUDED[] && senpi_needs_handles(parsed) && senpi_ensure_handles()
|
|
449
333
|
value = Core.eval(Main, parsed)
|
|
450
334
|
flush(stdout)
|
|
451
335
|
flush(stderr)
|
|
452
336
|
yield()
|
|
453
337
|
frame = Dict{String, Any}("type" => "result", "cellId" => cell_id, "ok" => true, "durationMs" => round(Int, (time() - started) * 1000))
|
|
454
|
-
value !== nothing && senpi_should_display_result(parsed)
|
|
455
|
-
|
|
456
|
-
|
|
338
|
+
if value !== nothing && senpi_should_display_result(parsed)
|
|
339
|
+
# Core.eval does not advance this caller's world after a first lazy include.
|
|
340
|
+
frame["valueRepr"] = SENPI_HANDLES_INCLUDED[] ? Base.invokelatest(senpi_json, value) : senpi_json(value)
|
|
341
|
+
end
|
|
457
342
|
senpi_emit(frame)
|
|
458
343
|
catch error
|
|
459
344
|
senpi_emit(Dict("type" => "result", "cellId" => cell_id, "ok" => false, "error" => senpi_error(error), "durationMs" => round(Int, (time() - started) * 1000)))
|
|
460
345
|
finally
|
|
461
346
|
global senpi_current_cell = nothing
|
|
347
|
+
global senpi_memory_cell = cell_id
|
|
462
348
|
end
|
|
463
349
|
end
|
|
464
350
|
|
|
@@ -472,10 +358,16 @@ while !eof(SENPI_ORIGINAL_STDIN)
|
|
|
472
358
|
if kind == "init"
|
|
473
359
|
senpi_emit(Dict("type" => "status", "event" => Dict("op" => "kernel-startup", "stage" => "host-init")))
|
|
474
360
|
senpi_set_connection(get(message, "connection", nothing))
|
|
475
|
-
|
|
476
|
-
senpi_emit(Dict("type" => "ready"))
|
|
361
|
+
senpi_emit(Dict("type" => "ready", "memoryGlobals" => true))
|
|
477
362
|
elseif kind == "run"
|
|
363
|
+
global senpi_memory_cell = nothing
|
|
478
364
|
senpi_run_cell(message)
|
|
365
|
+
elseif kind == "memory-globals"
|
|
366
|
+
cell_id = get(message, "cellId", nothing)
|
|
367
|
+
if cell_id == senpi_memory_cell && senpi_current_cell === nothing
|
|
368
|
+
senpi_emit(Dict("type" => "memory-globals-result", "cellId" => cell_id, "globals" => senpi_memory_globals(5)))
|
|
369
|
+
global senpi_memory_cell = nothing
|
|
370
|
+
end
|
|
479
371
|
elseif kind == "close"
|
|
480
372
|
senpi_emit(Dict("type" => "closed"))
|
|
481
373
|
break
|