@code-yeongyu/senpi-codemode 2026.9.28 → 2026.9.29-3
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 +123 -0
- package/README.md +13 -1
- package/package.json +4 -4
- package/src/bridge/memory-protocol.ts +39 -0
- package/src/bridge/protocol.ts +18 -0
- package/src/bridge/reserved.ts +2 -0
- package/src/config/memory-settings.ts +149 -0
- package/src/config/settings.ts +8 -2
- package/src/extension/session-manager-proxy.ts +6 -0
- package/src/extension/session-manager.ts +27 -25
- package/src/host-sdk.ts +1 -1
- package/src/index.ts +4 -0
- package/src/kernels/jl/kernel.ts +4 -1
- package/src/kernels/js/context-manager.ts +43 -5
- package/src/kernels/js/inline-worker.ts +2 -1
- package/src/kernels/js/interrupt-bounds.ts +8 -2
- package/src/kernels/js/kernel-contract.ts +13 -0
- package/src/kernels/js/local-module-loader.ts +2 -0
- package/src/kernels/js/webview-host.ts +59 -0
- package/src/kernels/js/worker-core.js +32 -3
- package/src/kernels/js/worker-global-sizes.js +123 -0
- package/src/kernels/js/worker-heap.js +59 -0
- package/src/kernels/js/worker-host.ts +2 -1
- package/src/kernels/js/worker-memory.js +94 -0
- package/src/kernels/js/worker-runtime.js +7 -2
- package/src/kernels/js/worker-slot.ts +16 -5
- package/src/kernels/js/worker-startup.ts +1 -0
- package/src/kernels/js/worker-webview-client.js +79 -0
- package/src/kernels/js/worker-webview.js +204 -0
- package/src/kernels/py/kernel-contract.ts +3 -0
- package/src/kernels/py/kernel.ts +15 -2
- package/src/kernels/py/prelude.py +327 -11
- package/src/kernels/py/transport.ts +4 -1
- package/src/kernels/rb/kernel.ts +4 -1
- package/src/kernels/shared/kernel-memory-host.ts +63 -0
- package/src/kernels/shared/kernel-memory.ts +136 -0
- package/src/kernels/shared/subprocess-contract.ts +10 -1
- package/src/kernels/shared/subprocess-kernel.ts +23 -2
- package/src/kernels/shared/subprocess-queue.ts +2 -0
- package/src/output/streaming-output-buffer.ts +229 -12
- package/src/output/streaming-output.ts +2 -2
- package/src/prompt/eval-prompt.ts +1 -1
- package/src/skill/bun-1-4/references/runtime-apis.md +1 -0
- package/src/tool/cell-runtime.ts +52 -10
- package/src/tool/detached-cell-contract.ts +4 -0
- package/src/tool/detached-cell-manager.ts +16 -1
- package/src/tool/detached-cell-notification.ts +10 -1
- package/src/tool/image.ts +6 -2
- package/src/tool/run-eval-cell.ts +19 -17
- package/src/tool/settled-image-spill.ts +108 -0
- package/src/tool/terminal-snapshot-store.ts +82 -10
- package/src/tool/types.ts +10 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { MessagePort } from "node:worker_threads";
|
|
1
2
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
3
|
import type { WorkerLike } from "./inline-worker.ts";
|
|
3
4
|
import { retireWorker, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
4
5
|
import type { JavaScriptKernelMode } from "./kernel-contract.ts";
|
|
5
6
|
import type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
7
|
+
import { KernelWebViewClients } from "./webview-host.ts";
|
|
6
8
|
import { WorkerStartupCancelledError } from "./worker-host.ts";
|
|
7
9
|
import { startWorkerWithInlineFallback } from "./worker-startup.ts";
|
|
8
10
|
|
|
@@ -17,6 +19,7 @@ export class WorkerSlot {
|
|
|
17
19
|
readonly #options: JavaScriptKernelOptions;
|
|
18
20
|
readonly #listeners: WorkerSlotListeners;
|
|
19
21
|
#worker: WorkerLike | null = null;
|
|
22
|
+
#webViews: KernelWebViewClients | null = null;
|
|
20
23
|
#mode: JavaScriptKernelMode = "worker";
|
|
21
24
|
#generation = 0;
|
|
22
25
|
#ready: Promise<void> | null = null;
|
|
@@ -43,8 +46,8 @@ export class WorkerSlot {
|
|
|
43
46
|
return this.#generation;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
postMessage(message: HostToKernelMessage): void {
|
|
47
|
-
this.#worker?.postMessage(message);
|
|
49
|
+
postMessage(message: HostToKernelMessage, transfer?: readonly MessagePort[]): void {
|
|
50
|
+
this.#worker?.postMessage(message, transfer);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
async ensureReady(): Promise<void> {
|
|
@@ -89,16 +92,24 @@ export class WorkerSlot {
|
|
|
89
92
|
this.#startupAbort = null;
|
|
90
93
|
this.#ready = null;
|
|
91
94
|
const worker = this.#worker;
|
|
95
|
+
const webViews = this.#webViews;
|
|
92
96
|
this.#worker = null;
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
this.#webViews = null;
|
|
98
|
+
const retirement = worker
|
|
99
|
+
? await retireWorker(worker, this.#options.interruptBounds?.terminateDeadlineMs)
|
|
100
|
+
: "terminated";
|
|
101
|
+
await webViews?.release();
|
|
102
|
+
return retirement;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
#publish(worker: WorkerLike, generation: number): void {
|
|
98
106
|
if (!this.#listeners.isOpen() || generation !== this.#generation) throw new WorkerStartupCancelledError();
|
|
99
107
|
this.#worker = worker;
|
|
108
|
+
const webViews = new KernelWebViewClients((message, transfer) => worker.postMessage(message, transfer));
|
|
109
|
+
this.#webViews = webViews;
|
|
100
110
|
worker.onMessage((message) => {
|
|
101
|
-
if (this.#isCurrent(worker, generation)
|
|
111
|
+
if (!this.#isCurrent(worker, generation) || webViews.consume(message)) return;
|
|
112
|
+
this.#listeners.onMessage(message);
|
|
102
113
|
});
|
|
103
114
|
worker.onError((error) => {
|
|
104
115
|
if (this.#isCurrent(worker, generation)) this.#listeners.onCrash(error);
|
|
@@ -67,6 +67,7 @@ async function initializeWorker(worker: WorkerLike, hooks: WorkerStartupHooks, s
|
|
|
67
67
|
hostToolNames: resolveKernelToolNameSource(options.hostToolNames),
|
|
68
68
|
foreignLanguageNames: resolveKernelToolNameSource(options.foreignLanguageNames),
|
|
69
69
|
...(options.sessionEnv === undefined ? {} : { sessionEnv: options.sessionEnv }),
|
|
70
|
+
...(options.memory === undefined ? {} : { memory: options.memory }),
|
|
70
71
|
});
|
|
71
72
|
await ready;
|
|
72
73
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The kernel worker's end of its private port to the main-thread Bun.WebView service. Mirrors
|
|
2
|
+
// the message shapes in packages/coding-agent/src/core/webview/webview-wire.ts (this worker file
|
|
3
|
+
// cannot import TypeScript); keep the two in sync.
|
|
4
|
+
|
|
5
|
+
export const DELIVER_EVENT = Symbol("senpi.webview.deliver");
|
|
6
|
+
|
|
7
|
+
export function webViewError(wire) {
|
|
8
|
+
const error = new Error(wire?.message ?? "Bun.WebView request failed");
|
|
9
|
+
if (typeof wire?.name === "string") error.name = wire.name;
|
|
10
|
+
if (typeof wire?.code === "string") error.code = wire.code;
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class WebViewPortClient {
|
|
15
|
+
#port;
|
|
16
|
+
#pending = new Map();
|
|
17
|
+
#views = new Map();
|
|
18
|
+
#nextId = 1;
|
|
19
|
+
#closed = false;
|
|
20
|
+
#onClosed;
|
|
21
|
+
|
|
22
|
+
constructor(port, onClosed) {
|
|
23
|
+
this.#port = port;
|
|
24
|
+
this.#onClosed = onClosed;
|
|
25
|
+
port.on("message", message => this.#receive(message));
|
|
26
|
+
port.on("close", () => this.#fail(new Error("Bun.WebView service connection closed")));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
register(viewId, view) {
|
|
30
|
+
this.#views.set(viewId, new WeakRef(view));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
forget(viewId) {
|
|
34
|
+
this.#views.delete(viewId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
request(message) {
|
|
38
|
+
if (this.#closed) return Promise.reject(new Error("Bun.WebView service connection closed"));
|
|
39
|
+
const id = this.#nextId++;
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
this.#pending.set(id, { resolve, reject });
|
|
42
|
+
try {
|
|
43
|
+
this.#port.postMessage({ ...message, id });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
this.#pending.delete(id);
|
|
46
|
+
reject(error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
post(message) {
|
|
52
|
+
if (this.#closed) return;
|
|
53
|
+
try {
|
|
54
|
+
this.#port.postMessage(message);
|
|
55
|
+
} catch {
|
|
56
|
+
// A closed port has already released every view on the service side.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#receive(message) {
|
|
61
|
+
if (message?.kind === "reply") {
|
|
62
|
+
const pending = this.#pending.get(message.id);
|
|
63
|
+
if (!pending) return;
|
|
64
|
+
this.#pending.delete(message.id);
|
|
65
|
+
if (message.ok) pending.resolve({ value: message.value, state: message.state });
|
|
66
|
+
else pending.reject(webViewError(message.error));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (message?.kind === "event") this.#views.get(message.viewId)?.deref()?.[DELIVER_EVENT](message.event);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#fail(error) {
|
|
73
|
+
if (this.#closed) return;
|
|
74
|
+
this.#closed = true;
|
|
75
|
+
for (const pending of this.#pending.values()) pending.reject(error);
|
|
76
|
+
this.#pending.clear();
|
|
77
|
+
this.#onClosed();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { DELIVER_EVENT, WebViewPortClient, webViewError } from "./worker-webview-client.js";
|
|
2
|
+
|
|
3
|
+
// Bun constructs "chrome"-backend WebViews only on the process main thread, and this kernel runs in
|
|
4
|
+
// a worker where `globalThis.Bun` and `Bun.WebView` are non-writable. Cells therefore run with a
|
|
5
|
+
// `Bun` parameter bound to a stand-in whose `WebView` forwards Chrome-backed views to the
|
|
6
|
+
// main-thread service over a private port; every other `Bun.*` member is the real one.
|
|
7
|
+
const KERNEL_BUN = Symbol.for("senpi.kernel.bun");
|
|
8
|
+
|
|
9
|
+
export function bindKernelBun(source) {
|
|
10
|
+
if (!globalThis[KERNEL_BUN]) return source;
|
|
11
|
+
return `((Bun) => ${source})(globalThis[Symbol.for("senpi.kernel.bun")])`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function installKernelWebView(requestPort) {
|
|
15
|
+
const bun = globalThis.Bun;
|
|
16
|
+
if (typeof bun !== "object" || bun === null || typeof bun.WebView !== "function") return;
|
|
17
|
+
globalThis[KERNEL_BUN] = shadowBun(bun, createKernelWebView(bun.WebView, requestPort));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shadowBun(real, WebView) {
|
|
21
|
+
return new Proxy(
|
|
22
|
+
{},
|
|
23
|
+
{
|
|
24
|
+
get: (_target, key) => (key === "WebView" ? WebView : Reflect.get(real, key)),
|
|
25
|
+
set: (_target, key, value) => Reflect.set(real, key, value),
|
|
26
|
+
has: (_target, key) => key === "WebView" || Reflect.has(real, key),
|
|
27
|
+
ownKeys: () => Reflect.ownKeys(real),
|
|
28
|
+
getOwnPropertyDescriptor: (_target, key) => {
|
|
29
|
+
if (key === "WebView") return { value: WebView, writable: false, enumerable: true, configurable: true };
|
|
30
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(real, key);
|
|
31
|
+
return descriptor && { ...descriptor, configurable: true };
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The macOS default (WebKit) stays a native worker view: its host process serves any thread.
|
|
38
|
+
function needsMainThread(options) {
|
|
39
|
+
const backend = options?.backend;
|
|
40
|
+
if (backend === undefined) return process.platform !== "darwin";
|
|
41
|
+
return backend === "chrome" || (typeof backend === "object" && backend !== null && backend.type === "chrome");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function closedError(method) {
|
|
45
|
+
const error = new Error(`Invalid state: WebView.${method}: view is closed`);
|
|
46
|
+
error.code = "ERR_INVALID_STATE";
|
|
47
|
+
return error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createKernelWebView(NativeWebView, requestPort) {
|
|
51
|
+
let connecting = null;
|
|
52
|
+
const proxied = new WeakSet();
|
|
53
|
+
const live = new Set();
|
|
54
|
+
const natives = new Set();
|
|
55
|
+
const finalizer = new FinalizationRegistry(({ client, viewId }) => client.post({ kind: "close", viewId }));
|
|
56
|
+
|
|
57
|
+
function connect() {
|
|
58
|
+
if (!connecting) {
|
|
59
|
+
const attempt = requestPort().then(port => new WebViewPortClient(port, () => (connecting = null)));
|
|
60
|
+
attempt.catch(() => {
|
|
61
|
+
if (connecting === attempt) connecting = null;
|
|
62
|
+
});
|
|
63
|
+
connecting = attempt;
|
|
64
|
+
}
|
|
65
|
+
return connecting;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class WebView extends EventTarget {
|
|
69
|
+
static closeAll() {
|
|
70
|
+
for (const view of [...live]) view.close();
|
|
71
|
+
for (const ref of [...natives]) ref.deref()?.close();
|
|
72
|
+
natives.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static [Symbol.hasInstance](value) {
|
|
76
|
+
return value instanceof NativeWebView || proxied.has(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#viewId = crypto.randomUUID();
|
|
80
|
+
#state = { url: "", title: "", loading: false };
|
|
81
|
+
#listening = new Set();
|
|
82
|
+
#console;
|
|
83
|
+
#ready;
|
|
84
|
+
#closed = false;
|
|
85
|
+
onNavigated = null;
|
|
86
|
+
onNavigationFailed = null;
|
|
87
|
+
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
if (!needsMainThread(options)) {
|
|
90
|
+
const view = new NativeWebView(options);
|
|
91
|
+
natives.add(new WeakRef(view));
|
|
92
|
+
return view;
|
|
93
|
+
}
|
|
94
|
+
super();
|
|
95
|
+
const { console: capture, ...rest } = options;
|
|
96
|
+
this.#console = capture;
|
|
97
|
+
proxied.add(this);
|
|
98
|
+
live.add(this);
|
|
99
|
+
const viewId = this.#viewId;
|
|
100
|
+
this.#ready = connect().then(async client => {
|
|
101
|
+
client.register(viewId, this);
|
|
102
|
+
finalizer.register(this, { client, viewId }, this);
|
|
103
|
+
const reply = await client.request({ kind: "create", viewId, options: rest, captureConsole: capture !== undefined });
|
|
104
|
+
this.#apply(reply.state);
|
|
105
|
+
return client;
|
|
106
|
+
});
|
|
107
|
+
this.#ready.catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get url() {
|
|
111
|
+
return this.#state.url;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get title() {
|
|
115
|
+
return this.#state.title;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get loading() {
|
|
119
|
+
return this.#state.loading;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
navigate(url) { return this.#call("navigate", [url]); }
|
|
123
|
+
evaluate(script) { return this.#call("evaluate", [script]); }
|
|
124
|
+
click(...args) { return this.#call("click", args); }
|
|
125
|
+
type(text) { return this.#call("type", [text]); }
|
|
126
|
+
press(...args) { return this.#call("press", args); }
|
|
127
|
+
scroll(dx, dy) { return this.#call("scroll", [dx, dy]); }
|
|
128
|
+
scrollTo(...args) { return this.#call("scrollTo", args); }
|
|
129
|
+
resize(width, height) { return this.#call("resize", [width, height]); }
|
|
130
|
+
back() { return this.#call("back", []); }
|
|
131
|
+
forward() { return this.#call("forward", []); }
|
|
132
|
+
reload() { return this.#call("reload", []); }
|
|
133
|
+
cdp(...args) { return this.#call("cdp", args); }
|
|
134
|
+
|
|
135
|
+
async screenshot(options) {
|
|
136
|
+
const value = await this.#call("screenshot", [options]);
|
|
137
|
+
if (options?.encoding === "buffer" && value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
addEventListener(type, listener, options) {
|
|
142
|
+
super.addEventListener(type, listener, options);
|
|
143
|
+
if (this.#closed || this.#listening.has(type)) return;
|
|
144
|
+
this.#listening.add(type);
|
|
145
|
+
const viewId = this.#viewId;
|
|
146
|
+
this.#ready.then(client => client.post({ kind: "listen", viewId, event: type }), () => {});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
close() {
|
|
150
|
+
if (this.#closed) return;
|
|
151
|
+
this.#closed = true;
|
|
152
|
+
live.delete(this);
|
|
153
|
+
const viewId = this.#viewId;
|
|
154
|
+
this.#ready.then(
|
|
155
|
+
client => {
|
|
156
|
+
client.post({ kind: "close", viewId });
|
|
157
|
+
client.forget(viewId);
|
|
158
|
+
finalizer.unregister(this);
|
|
159
|
+
},
|
|
160
|
+
() => {},
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
[Symbol.dispose]() {
|
|
165
|
+
this.close();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
[Symbol.asyncDispose]() {
|
|
169
|
+
this.close();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
[DELIVER_EVENT](event) {
|
|
173
|
+
if (event.type === "console") {
|
|
174
|
+
const [type, ...args] = event.args;
|
|
175
|
+
if (typeof this.#console === "function") this.#console(type, ...args);
|
|
176
|
+
else this.#console?.[type]?.(...args);
|
|
177
|
+
} else if (event.type === "navigated") {
|
|
178
|
+
this.#state = { ...this.#state, url: event.url, title: event.title };
|
|
179
|
+
this.onNavigated?.(event.url, event.title);
|
|
180
|
+
} else if (event.type === "navigation-failed") {
|
|
181
|
+
this.onNavigationFailed?.(webViewError(event.error));
|
|
182
|
+
} else if (event.type === "dom") {
|
|
183
|
+
this.dispatchEvent(new MessageEvent(event.name, { data: event.data }));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async #call(method, args) {
|
|
188
|
+
if (this.#closed) throw closedError(method);
|
|
189
|
+
const client = await this.#ready;
|
|
190
|
+
if (this.#closed) throw closedError(method);
|
|
191
|
+
const trimmed = [...args];
|
|
192
|
+
while (trimmed.length > 0 && trimmed.at(-1) === undefined) trimmed.pop();
|
|
193
|
+
const reply = await client.request({ kind: "call", viewId: this.#viewId, method, args: trimmed });
|
|
194
|
+
this.#apply(reply.state);
|
|
195
|
+
return reply.value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#apply(state) {
|
|
199
|
+
if (state) this.#state = state;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return WebView;
|
|
204
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { KernelMemoryThresholds } from "../../bridge/memory-protocol.ts";
|
|
1
2
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
3
|
import type { EvalKernelRunInput } from "../../tool/types.ts";
|
|
3
4
|
import type { SessionEnvironment } from "../session-env.ts";
|
|
@@ -15,6 +16,8 @@ export interface PythonKernelStartOptions {
|
|
|
15
16
|
readonly startupTimeoutMs?: number;
|
|
16
17
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
17
18
|
readonly spawnProcess?: KernelSpawnProcess;
|
|
19
|
+
/** Post-cell collection, notice, and ceiling thresholds sent on `init`; absent leaves memory unmanaged. */
|
|
20
|
+
readonly memory?: KernelMemoryThresholds;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export type PythonKernelRunOptions = EvalKernelRunInput;
|
package/src/kernels/py/kernel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
2
2
|
import type { KernelToolsInvokeOptions } from "../js/kernel-tools-types.ts";
|
|
3
3
|
import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
|
|
4
|
+
import { KernelMemoryHost } from "../shared/kernel-memory-host.ts";
|
|
4
5
|
import { KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
|
|
5
6
|
import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
|
|
6
7
|
import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
|
|
@@ -22,11 +23,13 @@ export class PythonKernel {
|
|
|
22
23
|
#closePromise: Promise<void> | null = null;
|
|
23
24
|
#failure: Error | null = null;
|
|
24
25
|
readonly #preludes = new KernelPreludeTracker();
|
|
26
|
+
readonly #memory: KernelMemoryHost | null;
|
|
25
27
|
#generation = 0;
|
|
26
28
|
#closed = false;
|
|
27
29
|
|
|
28
30
|
private constructor(options: PythonKernelStartOptions) {
|
|
29
31
|
this.#options = options;
|
|
32
|
+
this.#memory = options.memory === undefined ? null : new KernelMemoryHost("py", options.memory);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
static async start(options: PythonKernelStartOptions): Promise<PythonKernel> {
|
|
@@ -215,6 +218,7 @@ export class PythonKernel {
|
|
|
215
218
|
|
|
216
219
|
async #spawn(generation: number): Promise<void> {
|
|
217
220
|
if (this.#closed || generation !== this.#generation) throw new Error("Python kernel startup was superseded");
|
|
221
|
+
this.#memory?.processReplaced();
|
|
218
222
|
this.#transport = await PythonKernelTransport.start({
|
|
219
223
|
...this.#options,
|
|
220
224
|
onMessage: (message) => {
|
|
@@ -241,11 +245,20 @@ export class PythonKernel {
|
|
|
241
245
|
if (this.#transport !== transport) return;
|
|
242
246
|
const pending = this.#pending.get(result.cellId);
|
|
243
247
|
if (pending) {
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
const annotated = this.#memory?.annotate(result) ?? result;
|
|
249
|
+
(pending.input.onMessage ?? this.#options.onMessage)?.(annotated);
|
|
250
|
+
this.#settleRun(pending, annotated);
|
|
246
251
|
}
|
|
247
252
|
// A result frame from the live runner proves the process survived the interrupt.
|
|
248
253
|
if (pending?.resolveStateRetained) pending.resolveStateRetained(true);
|
|
254
|
+
this.#recycleOverCeilingWhenIdle();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** An over-ceiling kernel restarts only once no cell is running or queued on it. */
|
|
258
|
+
#recycleOverCeilingWhenIdle(): void {
|
|
259
|
+
if (!this.#memory?.claimRecycle(this.#active === null && this.#queue.length === 0)) return;
|
|
260
|
+
// A failed restart is recorded in #failure and rejects the next run; a superseded one lost to close/reset.
|
|
261
|
+
void this.reset().catch(() => undefined);
|
|
249
262
|
}
|
|
250
263
|
|
|
251
264
|
#onExit(transport: PythonKernelTransport, error: Error): void {
|