@code-yeongyu/senpi-codemode 2026.10.18 → 2026.10.19
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 +26 -0
- package/README.md +15 -0
- package/package.json +4 -4
- package/src/bridge/protocol.ts +8 -0
- package/src/extension/kernel-globals-ledger.ts +101 -0
- package/src/extension/kernel-replacement.ts +53 -5
- package/src/extension/session-manager.ts +18 -5
- package/src/kernels/py/death-reason.ts +29 -0
- package/src/kernels/py/kernel.ts +13 -3
- package/src/kernels/py/prelude.py +36 -6
- package/src/kernels/py/transport.ts +26 -3
- package/src/kernels/shared/host-memory-state.ts +65 -0
- package/src/kernels/shared/kernel-death.ts +22 -2
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,32 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.10.19] - 2026-10-19
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
## [2026.10.18-1] - 2026-10-18
|
|
28
|
+
|
|
29
|
+
### Breaking Changes
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- A Python kernel no longer loses its globals silently. When its control pipe broke before its exit was seen (a dying interpreter hit by the host's next write), the death went unreported and the next cell started a fresh interpreter with no notice; it is now a death like any other, replaced and announced. A kernel replaced by a later generation of the same session (a reload, including the automatic config reload, a host restart, or a resume) now announces itself on its first result. Restart notices name the lost globals; a SIGKILL that senpi did not send says it was probably the system under memory pressure and gives the host's memory state and the kernel's last memory reading; the prelude's parent watch names itself when it stops the kernel. Reported by 용짱 ([#3612](https://github.com/code-yeongyu/senpi/issues/3612)).
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
|
|
15
41
|
## [2026.10.18] - 2026-10-18
|
|
16
42
|
|
|
17
43
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -560,6 +560,21 @@ fresh interpreter dies too before it finished a cell, the queued cells fail with
|
|
|
560
560
|
another. An interpreter whose exit cannot be confirmed is reported, never
|
|
561
561
|
replaced by a second one. The JavaScript worker keeps its own restart path.
|
|
562
562
|
|
|
563
|
+
A death is reported however the host first hears of it: a broken control pipe
|
|
564
|
+
(EPIPE) counts the same as the exit. A SIGKILL senpi did not send reads
|
|
565
|
+
`signal 9, probably killed by the system, most often under memory pressure`,
|
|
566
|
+
with the host's memory state when it can be read (macOS memory pressure level
|
|
567
|
+
and swap in use; Linux available memory and swap in use) and, for Python, the
|
|
568
|
+
kernel's last memory reading. The Python prelude's parent watch names itself
|
|
569
|
+
instead when it stops the kernel. Python results report the user globals
|
|
570
|
+
defined so far (`globalNames`, at most 64, and `globalCount`); the notice lists
|
|
571
|
+
the ones a dead kernel took with it (`Lost: helper, rows`). The session keeps
|
|
572
|
+
that list in `<artifacts>/kernels/py-globals.json`, so the first result of a
|
|
573
|
+
kernel started by a later generation of the same session (a reload, including
|
|
574
|
+
the automatic config reload, a host restart, or a resume) says
|
|
575
|
+
`[py kernel is new: ...; every global it held is gone. Lost: ...]`. A reset
|
|
576
|
+
clears the list.
|
|
577
|
+
|
|
563
578
|
Commands a cell runs through `Bun.$` never read the host's terminal: the worker
|
|
564
579
|
thread shares the TUI's stdin, so the shell wrapper hands every template an
|
|
565
580
|
empty pipe (`true | ( … )`) while a cell is active. Output, exit codes, `cwd`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.19",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@babel/parser": "8.0.4",
|
|
36
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
36
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.19",
|
|
37
37
|
"quickjs-wasi": "3.6.2",
|
|
38
38
|
"typebox": "1.3.34"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
41
|
+
"@code-yeongyu/senpi": "2026.10.19"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
44
|
+
"@code-yeongyu/senpi": "2026.10.19"
|
|
45
45
|
},
|
|
46
46
|
"keywords": [
|
|
47
47
|
"senpi",
|
package/src/bridge/protocol.ts
CHANGED
|
@@ -100,6 +100,12 @@ const hostToKernelMessageSchema = Type.Union([
|
|
|
100
100
|
*/
|
|
101
101
|
const kernelStateSchema = Type.Union([Type.Literal("lost"), Type.Literal("restarted"), Type.Literal("not-run")]);
|
|
102
102
|
|
|
103
|
+
/** Python results: the user globals defined after the cell, so a notice can name what a lost kernel took with it. */
|
|
104
|
+
const resultGlobalsSchema = {
|
|
105
|
+
globalCount: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
106
|
+
globalNames: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { maxItems: 64 })),
|
|
107
|
+
};
|
|
108
|
+
|
|
103
109
|
const kernelToHostMessageSchema = Type.Union([
|
|
104
110
|
Type.Object({ type: Type.Literal("ready"), memoryGlobals: Type.Optional(Type.Boolean()) }),
|
|
105
111
|
Type.Object({ type: Type.Literal("init-failed"), error: bridgeErrorSchema }),
|
|
@@ -129,6 +135,7 @@ const kernelToHostMessageSchema = Type.Union([
|
|
|
129
135
|
valueRepr: Type.Optional(Type.String()),
|
|
130
136
|
durationMs: Type.Integer({ minimum: 0 }),
|
|
131
137
|
memory: Type.Optional(kernelMemoryReportSchema),
|
|
138
|
+
...resultGlobalsSchema,
|
|
132
139
|
/** Host-set: the bracketed notice that this cell ran on a kernel restarted after its interpreter died. */
|
|
133
140
|
notice: Type.Optional(Type.String()),
|
|
134
141
|
kernelState: Type.Optional(kernelStateSchema),
|
|
@@ -140,6 +147,7 @@ const kernelToHostMessageSchema = Type.Union([
|
|
|
140
147
|
error: bridgeErrorSchema,
|
|
141
148
|
durationMs: Type.Integer({ minimum: 0 }),
|
|
142
149
|
memory: Type.Optional(kernelMemoryReportSchema),
|
|
150
|
+
...resultGlobalsSchema,
|
|
143
151
|
notice: Type.Optional(Type.String()),
|
|
144
152
|
kernelState: Type.Optional(kernelStateSchema),
|
|
145
153
|
}),
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { LostGlobals } from "../kernels/shared/kernel-death.ts";
|
|
4
|
+
import type { EvalKernelResult, EvalLanguage } from "../tool/types.ts";
|
|
5
|
+
|
|
6
|
+
const NONE: LostGlobals = { names: [], count: 0 };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The user globals a session's kernel holds, as its newest result reported them, kept in memory and in
|
|
10
|
+
* the session's artifacts directory. A death names them in its restart notice; a later session-manager
|
|
11
|
+
* generation (reload, host restart, resume) reads the file and names what its predecessor took with it.
|
|
12
|
+
* The file is a diagnostic: when it cannot be read or written the notice names fewer globals, nothing else.
|
|
13
|
+
*/
|
|
14
|
+
export class KernelGlobalsLedger {
|
|
15
|
+
readonly #path: string | undefined;
|
|
16
|
+
#current: LostGlobals = NONE;
|
|
17
|
+
#inherited: LostGlobals;
|
|
18
|
+
#writes: Promise<void> = Promise.resolve();
|
|
19
|
+
#written = "";
|
|
20
|
+
|
|
21
|
+
private constructor(path: string | undefined, inherited: LostGlobals) {
|
|
22
|
+
this.#path = path;
|
|
23
|
+
this.#inherited = inherited;
|
|
24
|
+
this.#written = serialize(inherited);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static inMemory(): KernelGlobalsLedger {
|
|
28
|
+
return new KernelGlobalsLedger(undefined, NONE);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static async open(artifactsDir: string | undefined, language: EvalLanguage): Promise<KernelGlobalsLedger> {
|
|
32
|
+
if (artifactsDir === undefined) return KernelGlobalsLedger.inMemory();
|
|
33
|
+
const path = join(artifactsDir, "kernels", `${language}-globals.json`);
|
|
34
|
+
return new KernelGlobalsLedger(path, await readLedger(path));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What an earlier kernel of this session held when it ended, once; empty when it held nothing. */
|
|
38
|
+
takeInherited(): LostGlobals {
|
|
39
|
+
const inherited = this.#inherited;
|
|
40
|
+
this.#inherited = NONE;
|
|
41
|
+
return inherited;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
takeLost(): LostGlobals {
|
|
45
|
+
const lost = this.#current;
|
|
46
|
+
this.#record(NONE);
|
|
47
|
+
return lost;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
observe(result: EvalKernelResult): void {
|
|
51
|
+
if (result.globalNames === undefined) return;
|
|
52
|
+
this.#record({ names: result.globalNames, count: result.globalCount ?? result.globalNames.length });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A reset discards every global on purpose: nothing is lost that a later notice should name. */
|
|
56
|
+
clear(): void {
|
|
57
|
+
this.#inherited = NONE;
|
|
58
|
+
this.#record(NONE);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
flush(): Promise<void> {
|
|
62
|
+
return this.#writes;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#record(globals: LostGlobals): void {
|
|
66
|
+
this.#current = globals;
|
|
67
|
+
const path = this.#path;
|
|
68
|
+
const text = serialize(globals);
|
|
69
|
+
if (path === undefined || text === this.#written) return;
|
|
70
|
+
this.#written = text;
|
|
71
|
+
this.#writes = this.#writes.then(() => writeLedger(path, text)).catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function serialize(globals: LostGlobals): string {
|
|
76
|
+
return JSON.stringify({ names: globals.names, count: globals.count });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function writeLedger(path: string, text: string): Promise<void> {
|
|
80
|
+
await mkdir(dirname(path), { recursive: true });
|
|
81
|
+
const staging = `${path}.${process.pid}.tmp`;
|
|
82
|
+
await writeFile(staging, text);
|
|
83
|
+
await rename(staging, path);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function readLedger(path: string): Promise<LostGlobals> {
|
|
87
|
+
let parsed: unknown;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
90
|
+
} catch {
|
|
91
|
+
return NONE;
|
|
92
|
+
}
|
|
93
|
+
if (typeof parsed !== "object" || parsed === null) return NONE;
|
|
94
|
+
const names = "names" in parsed && Array.isArray(parsed.names) ? parsed.names.filter(isName) : [];
|
|
95
|
+
const count = "count" in parsed && typeof parsed.count === "number" ? parsed.count : names.length;
|
|
96
|
+
return { names, count: Math.max(count, names.length) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isName(value: unknown): value is string {
|
|
100
|
+
return typeof value === "string" && value.length > 0;
|
|
101
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { HostToKernelMessage } from "../bridge/protocol.ts";
|
|
2
|
+
import { type HostMemoryReader, readHostMemoryState } from "../kernels/shared/host-memory-state.ts";
|
|
2
3
|
import {
|
|
3
4
|
type KernelLifecycle,
|
|
4
5
|
KernelUnavailableError,
|
|
6
|
+
type LostGlobals,
|
|
7
|
+
replacedKernelNotice,
|
|
5
8
|
restartNotice,
|
|
6
9
|
unstartedResult,
|
|
7
10
|
} from "../kernels/shared/kernel-death.ts";
|
|
@@ -13,10 +16,21 @@ import type {
|
|
|
13
16
|
KernelInterruptHandle,
|
|
14
17
|
PendingCell,
|
|
15
18
|
} from "../tool/types.ts";
|
|
19
|
+
import { KernelGlobalsLedger } from "./kernel-globals-ledger.ts";
|
|
16
20
|
import { hasKernelTools, type KernelToolsMethods } from "./kernel-tools-probe.ts";
|
|
17
21
|
|
|
18
22
|
export type StartKernel = (lifecycle: Required<KernelLifecycle>) => Promise<EvalKernel>;
|
|
19
23
|
|
|
24
|
+
export interface ReplaceableKernelOptions {
|
|
25
|
+
/** Names the globals a death or an earlier generation of the session took with it. */
|
|
26
|
+
readonly ledger?: KernelGlobalsLedger;
|
|
27
|
+
/** Read when a SIGKILL nobody in senpi sent explains a death; the default asks the OS. */
|
|
28
|
+
readonly readHostMemory?: HostMemoryReader;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** An unlabeled SIGKILL reaching a live host came from outside senpi: on a desktop, most often the OS under memory pressure. */
|
|
32
|
+
const SIGKILL_REASON = "signal 9";
|
|
33
|
+
|
|
20
34
|
class ReplaceableKernelClosedError extends Error {
|
|
21
35
|
readonly name = "ReplaceableKernelClosedError";
|
|
22
36
|
|
|
@@ -50,14 +64,25 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
50
64
|
#announce: string | null = null;
|
|
51
65
|
#closed = false;
|
|
52
66
|
#closeFailure: unknown;
|
|
67
|
+
readonly #ledger: KernelGlobalsLedger;
|
|
68
|
+
readonly #readHostMemory: HostMemoryReader;
|
|
69
|
+
/** What the instance held when it died, named by the replacement's notice. */
|
|
70
|
+
#lostAtDeath: LostGlobals | undefined;
|
|
53
71
|
|
|
54
72
|
// Present only when the instance has kernel tools, so a probe sees what it would see on the instance itself.
|
|
55
73
|
declare readonly describeKernelTools?: KernelToolsMethods["describeKernelTools"];
|
|
56
74
|
declare readonly invokeKernelTool?: KernelToolsMethods["invokeKernelTool"];
|
|
57
75
|
|
|
58
|
-
private constructor(
|
|
76
|
+
private constructor(
|
|
77
|
+
language: EvalLanguage,
|
|
78
|
+
start: StartKernel,
|
|
79
|
+
first: EvalKernel,
|
|
80
|
+
options: ReplaceableKernelOptions,
|
|
81
|
+
) {
|
|
59
82
|
this.#language = language;
|
|
60
83
|
this.#start = start;
|
|
84
|
+
this.#ledger = options.ledger ?? KernelGlobalsLedger.inMemory();
|
|
85
|
+
this.#readHostMemory = options.readHostMemory ?? readHostMemoryState;
|
|
61
86
|
this.#kernel = first;
|
|
62
87
|
// Python is the one subprocess kernel with kernel tools; rb and jl only carry tools_unavailable stubs to hide.
|
|
63
88
|
if (language === "py" && hasKernelTools(first)) {
|
|
@@ -77,7 +102,11 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
/** Starts the first instance; one that cannot hand back its queue is returned as it is. */
|
|
80
|
-
static async create(
|
|
105
|
+
static async create(
|
|
106
|
+
language: EvalLanguage,
|
|
107
|
+
start: StartKernel,
|
|
108
|
+
options: ReplaceableKernelOptions = {},
|
|
109
|
+
): Promise<EvalKernel> {
|
|
81
110
|
let owner: ReplaceableKernel | undefined;
|
|
82
111
|
const first = await start({
|
|
83
112
|
onDeath: (reason) => {
|
|
@@ -85,7 +114,7 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
85
114
|
},
|
|
86
115
|
});
|
|
87
116
|
if (first.drainPending === undefined) return first;
|
|
88
|
-
owner = new ReplaceableKernel(language, start, first);
|
|
117
|
+
owner = new ReplaceableKernel(language, start, first, options);
|
|
89
118
|
return owner;
|
|
90
119
|
}
|
|
91
120
|
|
|
@@ -123,6 +152,7 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
123
152
|
}
|
|
124
153
|
|
|
125
154
|
async reset(): Promise<void> {
|
|
155
|
+
this.#ledger.clear();
|
|
126
156
|
if (this.#replacing !== null) await this.#replacing;
|
|
127
157
|
if (this.#kernel.isAlive?.() === false) {
|
|
128
158
|
// A reset discards every global anyway, so the fresh instance announces nothing.
|
|
@@ -144,6 +174,7 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
144
174
|
if (this.#replacing !== null) await this.#replacing;
|
|
145
175
|
if (this.#closeFailure !== undefined) throw this.#closeFailure;
|
|
146
176
|
await this.#kernel.close();
|
|
177
|
+
await this.#ledger.flush();
|
|
147
178
|
}
|
|
148
179
|
|
|
149
180
|
listKernelToolNames(): readonly string[] {
|
|
@@ -159,7 +190,10 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
159
190
|
if (kernel.isAlive?.() === false)
|
|
160
191
|
return result.kernelState === undefined ? { ...result, kernelState: "lost" } : result;
|
|
161
192
|
this.#recovering = null;
|
|
162
|
-
|
|
193
|
+
this.#ledger.observe(result);
|
|
194
|
+
const inherited = this.#ledger.takeInherited();
|
|
195
|
+
const notice =
|
|
196
|
+
this.#announce ?? (inherited.count > 0 ? replacedKernelNotice(this.#language, inherited) : null);
|
|
163
197
|
if (notice === null) return result;
|
|
164
198
|
this.#announce = null;
|
|
165
199
|
return { ...result, notice, kernelState: "restarted" };
|
|
@@ -169,6 +203,7 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
169
203
|
#died(generation: number, reason: string): void {
|
|
170
204
|
if (generation !== this.#generation || this.#closed) return;
|
|
171
205
|
this.#held = [...(this.#kernel.drainPending?.() ?? []), ...this.#held];
|
|
206
|
+
this.#lostAtDeath = this.#ledger.takeLost();
|
|
172
207
|
if (this.#recovering !== null) {
|
|
173
208
|
this.#recovering = null;
|
|
174
209
|
this.#unannounced = reason;
|
|
@@ -194,6 +229,9 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
194
229
|
/** Never rejects: every failure settles the held cells with what went wrong. */
|
|
195
230
|
async #replace(reason: string | null): Promise<void> {
|
|
196
231
|
const dead = this.#kernel;
|
|
232
|
+
// Composed while the old interpreter retires, and before the replacement starts: a death of the
|
|
233
|
+
// replacement must find it already current, with no await between its start and its adoption.
|
|
234
|
+
const announcing = reason === null ? Promise.resolve(null) : this.#restartNotice(reason);
|
|
197
235
|
try {
|
|
198
236
|
await dead.close();
|
|
199
237
|
} catch (error) {
|
|
@@ -201,6 +239,7 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
201
239
|
this.#failUnavailable(reason, `its interpreter could not be retired: ${errorText(error)}`);
|
|
202
240
|
return;
|
|
203
241
|
}
|
|
242
|
+
const announce = await announcing;
|
|
204
243
|
if (this.#closed) return;
|
|
205
244
|
const generation = ++this.#nextGeneration;
|
|
206
245
|
let next: EvalKernel;
|
|
@@ -218,7 +257,7 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
218
257
|
}
|
|
219
258
|
this.#kernel = next;
|
|
220
259
|
this.#generation = generation;
|
|
221
|
-
this.#announce =
|
|
260
|
+
this.#announce = announce;
|
|
222
261
|
for (const cell of this.#held.splice(0)) {
|
|
223
262
|
void this.#submit(generation, next, cell.input).then(cell.settle, (error: unknown) =>
|
|
224
263
|
cell.settle(unstartedResult(cell.input.cellId, errorText(error))),
|
|
@@ -226,6 +265,15 @@ export class ReplaceableKernel implements EvalKernel {
|
|
|
226
265
|
}
|
|
227
266
|
}
|
|
228
267
|
|
|
268
|
+
async #restartNotice(reason: string): Promise<string> {
|
|
269
|
+
const lost = this.#lostAtDeath;
|
|
270
|
+
this.#lostAtDeath = undefined;
|
|
271
|
+
if (!reason.startsWith(SIGKILL_REASON)) return restartNotice(this.#language, reason, lost);
|
|
272
|
+
const memory = await this.#readHostMemory();
|
|
273
|
+
const cause = `probably killed by the system, most often under memory pressure${memory === undefined ? "" : ` (${memory})`}`;
|
|
274
|
+
return restartNotice(this.#language, `${reason}, ${cause}`, lost);
|
|
275
|
+
}
|
|
276
|
+
|
|
229
277
|
#failUnavailable(reason: string | null, detail: string): void {
|
|
230
278
|
this.#recovering = null;
|
|
231
279
|
this.#unannounced = reason;
|
|
@@ -16,6 +16,7 @@ import { type BridgeDispatchContext, dispatchBridgeCompletion } from "./bridge-d
|
|
|
16
16
|
import { type BridgeToolCallRequest, routeBridgeToolCall } from "./bridge-tool-call.ts";
|
|
17
17
|
import { CellKernelTools } from "./cell-kernel-tools.ts";
|
|
18
18
|
import { parkWhenIdle } from "./idle-parking-kernel.ts";
|
|
19
|
+
import { KernelGlobalsLedger } from "./kernel-globals-ledger.ts";
|
|
19
20
|
import {
|
|
20
21
|
javaScriptKernelMemory,
|
|
21
22
|
SessionKernelRegistrations,
|
|
@@ -69,6 +70,8 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
69
70
|
readonly #registrations: SessionKernelRegistrations;
|
|
70
71
|
#kernelCreations = new Map<EvalLanguage, Promise<EvalKernel>>();
|
|
71
72
|
#onMessageRefs = new Map<EvalLanguage, (message: KernelToHostMessage) => void>();
|
|
73
|
+
/** One per language for the manager's life, so an idle-park restart never re-announces an earlier generation. */
|
|
74
|
+
readonly #ledgers = new Map<EvalLanguage, Promise<KernelGlobalsLedger>>();
|
|
72
75
|
readonly #cellKernelTools = new CellKernelTools();
|
|
73
76
|
#context: ExtensionContext | undefined;
|
|
74
77
|
#generation = 0;
|
|
@@ -231,11 +234,21 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
231
234
|
): Promise<{ readonly kernel: EvalKernel; readonly memory: RegisteredKernelSource | undefined }> {
|
|
232
235
|
if (language === "js") return await this.#createKernel(language, onMessage);
|
|
233
236
|
let memory: RegisteredKernelSource | undefined;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
237
|
+
let ledger = this.#ledgers.get(language);
|
|
238
|
+
if (ledger === undefined) {
|
|
239
|
+
ledger = KernelGlobalsLedger.open(this.#options.artifactsDir, language);
|
|
240
|
+
this.#ledgers.set(language, ledger);
|
|
241
|
+
}
|
|
242
|
+
const options = { ledger: await ledger };
|
|
243
|
+
const kernel = await ReplaceableKernel.create(
|
|
244
|
+
language,
|
|
245
|
+
async (lifecycle) => {
|
|
246
|
+
const created = await this.#createKernel(language, onMessage, lifecycle);
|
|
247
|
+
memory ??= created.memory;
|
|
248
|
+
return created.kernel;
|
|
249
|
+
},
|
|
250
|
+
options,
|
|
251
|
+
);
|
|
239
252
|
return { kernel, memory };
|
|
240
253
|
}
|
|
241
254
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { KernelMemoryReport } from "../../bridge/memory-protocol.ts";
|
|
2
|
+
import { describeExit } from "../shared/kernel-death.ts";
|
|
3
|
+
import { formatBytes } from "../shared/kernel-memory.ts";
|
|
4
|
+
|
|
5
|
+
/** Written by the prelude's parent watch (prelude.py `PARENT_WATCH_MARKER`) just before it kills its own group. */
|
|
6
|
+
const PARENT_WATCH_MARKER = "[senpi] parent watch:";
|
|
7
|
+
|
|
8
|
+
export interface PythonExit {
|
|
9
|
+
readonly code: number | null;
|
|
10
|
+
readonly signal: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Why an interpreter the host did not retire is gone, for the restart notice: the prelude's own parent
|
|
15
|
+
* watch names itself on stderr; anything else is its exit status, with the last memory the kernel
|
|
16
|
+
* reported so a reader can tell whether its own size could explain a kill by the system.
|
|
17
|
+
*/
|
|
18
|
+
export function pythonDeathReason(stderrTail: string, exit: PythonExit, lastMemory?: KernelMemoryReport): string {
|
|
19
|
+
const watch = stderrTail
|
|
20
|
+
.split("\n")
|
|
21
|
+
.find((line) => line.startsWith(PARENT_WATCH_MARKER))
|
|
22
|
+
?.slice(PARENT_WATCH_MARKER.length)
|
|
23
|
+
.trim();
|
|
24
|
+
if (watch !== undefined) return `senpi's parent watch stopped it (${watch})`;
|
|
25
|
+
const status = describeExit(exit.code, exit.signal);
|
|
26
|
+
if (lastMemory === undefined) return status;
|
|
27
|
+
const measure = lastMemory.measure === "heap" ? "live after GC" : "in its process footprint";
|
|
28
|
+
return `${status} (its last reading was ${formatBytes(lastMemory.liveBytes)} ${measure})`;
|
|
29
|
+
}
|
package/src/kernels/py/kernel.ts
CHANGED
|
@@ -6,9 +6,9 @@ import type {
|
|
|
6
6
|
KernelToolsInvokeRequest,
|
|
7
7
|
} from "../js/kernel-tools-types.ts";
|
|
8
8
|
import { inputAtStart } from "../shared/cell-source-at-start.ts";
|
|
9
|
-
import { describeExit } from "../shared/kernel-death.ts";
|
|
10
9
|
import { KernelMemoryHost } from "../shared/kernel-memory-host.ts";
|
|
11
10
|
import { KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
|
|
11
|
+
import { pythonDeathReason } from "./death-reason.ts";
|
|
12
12
|
import { runHostCell } from "./host-cell.ts";
|
|
13
13
|
import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
|
|
14
14
|
import { PythonKernelTools } from "./kernel-tools-host.ts";
|
|
@@ -41,6 +41,8 @@ export class PythonKernel {
|
|
|
41
41
|
#failure: Error | null = null;
|
|
42
42
|
readonly #preludes = new KernelPreludeTracker();
|
|
43
43
|
readonly #memory: KernelMemoryHost | null;
|
|
44
|
+
/** The newest reading the interpreter reported, named by a restart notice if it dies. */
|
|
45
|
+
#lastMemory: ResultMessage["memory"];
|
|
44
46
|
#generation = 0;
|
|
45
47
|
#closed = false;
|
|
46
48
|
readonly #tools: PythonKernelTools;
|
|
@@ -313,6 +315,7 @@ export class PythonKernel {
|
|
|
313
315
|
async #spawn(generation: number): Promise<void> {
|
|
314
316
|
if (this.#closed || generation !== this.#generation) throw new Error("Python kernel startup was superseded");
|
|
315
317
|
this.#memory?.processReplaced();
|
|
318
|
+
this.#lastMemory = undefined;
|
|
316
319
|
this.#transport = await PythonKernelTransport.start({
|
|
317
320
|
...this.#options,
|
|
318
321
|
onMessage: (message) => {
|
|
@@ -334,12 +337,14 @@ export class PythonKernel {
|
|
|
334
337
|
},
|
|
335
338
|
onResult: (transport, result) => this.#onResult(transport, result),
|
|
336
339
|
onError: (transport, error) => this.#onError(transport, error),
|
|
337
|
-
onExit: (transport, error, exit) =>
|
|
340
|
+
onExit: (transport, error, exit) =>
|
|
341
|
+
this.#onExit(transport, error, pythonDeathReason(error.message, exit, this.#lastMemory)),
|
|
338
342
|
});
|
|
339
343
|
}
|
|
340
344
|
|
|
341
345
|
#onResult(transport: PythonKernelTransport, result: ResultMessage): void {
|
|
342
346
|
if (this.#transport !== transport) return;
|
|
347
|
+
if (result.memory !== undefined) this.#lastMemory = result.memory;
|
|
343
348
|
const pending = this.#pending.get(result.cellId);
|
|
344
349
|
if (pending) {
|
|
345
350
|
const annotated = this.#memory?.annotate(result) ?? result;
|
|
@@ -403,11 +408,16 @@ export class PythonKernel {
|
|
|
403
408
|
);
|
|
404
409
|
},
|
|
405
410
|
);
|
|
411
|
+
// With an owner, the retired interpreter is a death like an exit (senpi#3612): replaced, and announced.
|
|
412
|
+
const onDeath = this.#options.onDeath;
|
|
413
|
+
if (onDeath) this.#dead = true;
|
|
406
414
|
const active = this.#active;
|
|
407
415
|
if (active) {
|
|
408
416
|
if (active.resolveStateRetained) active.resolveStateRetained(false);
|
|
409
|
-
|
|
417
|
+
const message = onDeath ? "Python kernel died; every global is lost" : "Python kernel died";
|
|
418
|
+
this.#settleRun(active, failedPythonResult(active.input.cellId, message, error.message));
|
|
410
419
|
}
|
|
420
|
+
onDeath?.(`a failure of its control pipe (${error.message})`);
|
|
411
421
|
}
|
|
412
422
|
|
|
413
423
|
#settleRun(pending: PendingRun, result: ResultMessage): void {
|
|
@@ -2172,6 +2172,12 @@ async def run_code(code: Any, want_value: bool) -> Any:
|
|
|
2172
2172
|
return None
|
|
2173
2173
|
|
|
2174
2174
|
|
|
2175
|
+
# Globals a tool prelude installs: the host reinstalls them on a fresh kernel, so they are never reported as lost.
|
|
2176
|
+
_PRELUDE_EXPORTS: set[str] = set()
|
|
2177
|
+
# The host keeps the newest list so a notice can name what a dead or replaced kernel took with it.
|
|
2178
|
+
_REPORTED_GLOBAL_NAMES = 64
|
|
2179
|
+
|
|
2180
|
+
|
|
2175
2181
|
def apply_preludes(preludes: Any) -> None:
|
|
2176
2182
|
# Host-computed per cell: globals of tools deactivated since the last cell are dropped,
|
|
2177
2183
|
# and an active tool's snippet runs only while one of its exports is missing.
|
|
@@ -2179,11 +2185,22 @@ def apply_preludes(preludes: Any) -> None:
|
|
|
2179
2185
|
return
|
|
2180
2186
|
for name in preludes.get("remove", []):
|
|
2181
2187
|
USER_NS.pop(name, None)
|
|
2188
|
+
_PRELUDE_EXPORTS.discard(name)
|
|
2182
2189
|
for contribution in preludes.get("install", []):
|
|
2183
|
-
|
|
2190
|
+
exports = contribution.get("exports", [])
|
|
2191
|
+
_PRELUDE_EXPORTS.update(exports)
|
|
2192
|
+
if any(name not in USER_NS for name in exports):
|
|
2184
2193
|
exec(compile(contribution.get("python", ""), "<kernel-prelude>", "exec"), USER_NS)
|
|
2185
2194
|
|
|
2186
2195
|
|
|
2196
|
+
def _user_global_names() -> list[str]:
|
|
2197
|
+
return sorted(
|
|
2198
|
+
name
|
|
2199
|
+
for name in list(USER_NS)
|
|
2200
|
+
if name not in _MEMORY_BASELINE and name not in _PRELUDE_EXPORTS and not name.startswith("__")
|
|
2201
|
+
)
|
|
2202
|
+
|
|
2203
|
+
|
|
2187
2204
|
def _enter_source_file(source_file: str | None) -> Callable[[], None]:
|
|
2188
2205
|
# A %load cell runs as its file: __file__ names it and its directory comes first on the import path,
|
|
2189
2206
|
# so `from sibling import x` resolves next to the file the way it does when the file runs as a script.
|
|
@@ -2272,6 +2289,9 @@ def run_cell(
|
|
|
2272
2289
|
text("stderr", f"[senpi] kernel memory measurement failed: {exc}\n")
|
|
2273
2290
|
if memory is not None:
|
|
2274
2291
|
result["memory"] = memory
|
|
2292
|
+
names = _user_global_names()
|
|
2293
|
+
result["globalCount"] = len(names)
|
|
2294
|
+
result["globalNames"] = names[:_REPORTED_GLOBAL_NAMES]
|
|
2275
2295
|
emit(result)
|
|
2276
2296
|
|
|
2277
2297
|
|
|
@@ -2334,12 +2354,21 @@ def _activate_env_root(root: str) -> None:
|
|
|
2334
2354
|
importlib.invalidate_caches()
|
|
2335
2355
|
|
|
2336
2356
|
|
|
2337
|
-
|
|
2357
|
+
PARENT_WATCH_MARKER = "[senpi] parent watch:"
|
|
2358
|
+
|
|
2359
|
+
|
|
2360
|
+
def _terminate_process_group(reason: str) -> None:
|
|
2338
2361
|
# The kernel is spawned into its own session (setsid), so its pid is its process
|
|
2339
2362
|
# group id and a cell's subprocesses inherit that group. Killing the group takes
|
|
2340
2363
|
# those children down with the kernel instead of orphaning them to init.
|
|
2364
|
+
# The reason goes to the real stderr first: a host that is still alive names it in its restart notice.
|
|
2341
2365
|
if os.name != "posix":
|
|
2342
2366
|
return
|
|
2367
|
+
stream = sys.__stderr__
|
|
2368
|
+
if stream is not None:
|
|
2369
|
+
with contextlib.suppress(OSError, ValueError):
|
|
2370
|
+
stream.write(f"{PARENT_WATCH_MARKER} {reason}; stopping the kernel\n")
|
|
2371
|
+
stream.flush()
|
|
2343
2372
|
with contextlib.suppress(OSError):
|
|
2344
2373
|
os.killpg(os.getpgrp(), signal.SIGKILL)
|
|
2345
2374
|
|
|
@@ -2350,8 +2379,9 @@ def _watch_parent(initial_ppid: int) -> None:
|
|
|
2350
2379
|
# daemon thread notices the reparenting instead and takes the whole group down.
|
|
2351
2380
|
while True:
|
|
2352
2381
|
time.sleep(1.0)
|
|
2353
|
-
|
|
2354
|
-
|
|
2382
|
+
current_ppid = os.getppid()
|
|
2383
|
+
if current_ppid != initial_ppid:
|
|
2384
|
+
_terminate_process_group(f"its parent process changed from {initial_ppid} to {current_ppid}")
|
|
2355
2385
|
return
|
|
2356
2386
|
|
|
2357
2387
|
|
|
@@ -2366,7 +2396,7 @@ def _watch_named_parent(parent_pid: int) -> None:
|
|
|
2366
2396
|
try:
|
|
2367
2397
|
os.kill(parent_pid, 0)
|
|
2368
2398
|
except ProcessLookupError:
|
|
2369
|
-
_terminate_process_group()
|
|
2399
|
+
_terminate_process_group(f"its host process {parent_pid} is gone")
|
|
2370
2400
|
return
|
|
2371
2401
|
|
|
2372
2402
|
|
|
@@ -2422,7 +2452,7 @@ def main() -> None:
|
|
|
2422
2452
|
# Reaching here without a close frame means the host's pipe hit EOF: it is gone,
|
|
2423
2453
|
# so retire any subprocess the last cell left running before the interpreter exits.
|
|
2424
2454
|
if not host_closed:
|
|
2425
|
-
_terminate_process_group()
|
|
2455
|
+
_terminate_process_group("its host closed the control pipe")
|
|
2426
2456
|
|
|
2427
2457
|
|
|
2428
2458
|
if __name__ == "__main__":
|
|
@@ -70,6 +70,8 @@ export interface PythonTransportOptions {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const hardKillWaitMs = 500;
|
|
73
|
+
/** How long a broken control pipe waits for the interpreter's exit, which carries the real cause of the death. */
|
|
74
|
+
const pipeErrorExitWaitMs = 1_000;
|
|
73
75
|
|
|
74
76
|
export interface PythonPreludePathOptions extends CodemodeRuntimeAssetEnvironment {
|
|
75
77
|
readonly localPath?: string;
|
|
@@ -95,6 +97,7 @@ export class PythonKernelTransport {
|
|
|
95
97
|
#retirement: Promise<void> | null = null;
|
|
96
98
|
#gone: Promise<void> | null = null;
|
|
97
99
|
#isGone = false;
|
|
100
|
+
#pipeErrorTimer: NodeJS.Timeout | null = null;
|
|
98
101
|
|
|
99
102
|
private constructor(options: PythonTransportOptions, child: KernelChild) {
|
|
100
103
|
this.#options = options;
|
|
@@ -245,9 +248,11 @@ export class PythonKernelTransport {
|
|
|
245
248
|
this.#child.on("error", onError);
|
|
246
249
|
this.#child.on("exit", onExit);
|
|
247
250
|
// senpi#3016: a frame written while the interpreter is dying fails on the stdin stream (EPIPE), not on the child.
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
this.#child.stdin.on("error",
|
|
251
|
+
// The listener is never detached, so a late write failure is never unhandled. senpi#3612: that failure can be
|
|
252
|
+
// processed before the child's exit; the exit then reports the death, with its signal.
|
|
253
|
+
this.#child.stdin.on("error", (error: unknown) =>
|
|
254
|
+
this.#onPipeError(error instanceof Error ? error : new Error(String(error))),
|
|
255
|
+
);
|
|
251
256
|
const { sessionId, connection, memory, kernelGeneration } = this.#options;
|
|
252
257
|
this.#write({
|
|
253
258
|
type: "init",
|
|
@@ -305,6 +310,7 @@ export class PythonKernelTransport {
|
|
|
305
310
|
#onExit(code: number | null, signal: string | null): void {
|
|
306
311
|
if (this.#exited) return;
|
|
307
312
|
this.#exited = true;
|
|
313
|
+
this.#clearPipeErrorTimer();
|
|
308
314
|
const active = this.#active;
|
|
309
315
|
this.#active = false;
|
|
310
316
|
const error = new Error(this.#stderrTail.trim() || `Python kernel exited (${code ?? signal ?? "unknown"})`);
|
|
@@ -315,10 +321,27 @@ export class PythonKernelTransport {
|
|
|
315
321
|
|
|
316
322
|
#onError(error: Error): void {
|
|
317
323
|
if (this.#exited || !this.#active) return;
|
|
324
|
+
this.#clearPipeErrorTimer();
|
|
318
325
|
this.#active = false;
|
|
319
326
|
if (!this.#settleStartup(error)) this.#options.onError(this, error);
|
|
320
327
|
}
|
|
321
328
|
|
|
329
|
+
/** A broken control pipe means the interpreter is gone or going: its exit, not this error, reports the death. */
|
|
330
|
+
#onPipeError(error: Error): void {
|
|
331
|
+
if (this.#exited || !this.#active || this.#pipeErrorTimer !== null) return;
|
|
332
|
+
this.#pipeErrorTimer = setTimeout(() => {
|
|
333
|
+
this.#pipeErrorTimer = null;
|
|
334
|
+
this.#onError(error);
|
|
335
|
+
}, pipeErrorExitWaitMs);
|
|
336
|
+
this.#pipeErrorTimer.unref?.();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#clearPipeErrorTimer(): void {
|
|
340
|
+
if (this.#pipeErrorTimer === null) return;
|
|
341
|
+
clearTimeout(this.#pipeErrorTimer);
|
|
342
|
+
this.#pipeErrorTimer = null;
|
|
343
|
+
}
|
|
344
|
+
|
|
322
345
|
#detachListeners(): void {
|
|
323
346
|
const detach = this.#detachChildListeners;
|
|
324
347
|
if (!detach) return;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { formatBytes } from "./kernel-memory.ts";
|
|
4
|
+
|
|
5
|
+
const READ_TIMEOUT_MS = 1_000;
|
|
6
|
+
const MIB = 1024 * 1024;
|
|
7
|
+
const KIB = 1024;
|
|
8
|
+
|
|
9
|
+
/** kern.memorystatus_vm_pressure_level: the kernel's own verdict, the one jetsam acts on. */
|
|
10
|
+
const DARWIN_PRESSURE_LEVELS: Readonly<Record<string, string>> = { "1": "normal", "2": "warning", "4": "critical" };
|
|
11
|
+
|
|
12
|
+
export type HostMemoryReader = () => Promise<string | undefined>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The host's memory state in a few words, for a notice about a kernel the system may have killed:
|
|
16
|
+
* macOS memory pressure and swap in use, Linux available memory and swap in use. `undefined` when the
|
|
17
|
+
* platform has no reading or it could not be taken in time; the notice then simply says less.
|
|
18
|
+
*/
|
|
19
|
+
export const readHostMemoryState: HostMemoryReader = async () => {
|
|
20
|
+
try {
|
|
21
|
+
if (process.platform === "darwin") return describeDarwin(await sysctl());
|
|
22
|
+
if (process.platform === "linux") return describeLinux(await readFile("/proc/meminfo", "utf8"));
|
|
23
|
+
return undefined;
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function sysctl(): Promise<string> {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
execFile(
|
|
32
|
+
"sysctl",
|
|
33
|
+
["-n", "kern.memorystatus_vm_pressure_level", "vm.swapusage"],
|
|
34
|
+
{ encoding: "utf8", timeout: READ_TIMEOUT_MS, env: { ...process.env, LC_ALL: "C" } },
|
|
35
|
+
(error, stdout) => (error ? reject(error) : resolve(stdout)),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function describeDarwin(output: string): string | undefined {
|
|
41
|
+
const [levelLine = "", swapLine = ""] = output.trim().split("\n");
|
|
42
|
+
const parts: string[] = [];
|
|
43
|
+
const level = DARWIN_PRESSURE_LEVELS[levelLine.trim()];
|
|
44
|
+
if (level !== undefined) parts.push(`system memory pressure ${level}`);
|
|
45
|
+
const used = /used = ([\d.]+)M/u.exec(swapLine)?.[1];
|
|
46
|
+
if (used !== undefined) parts.push(`swap used ${formatBytes(Number(used) * MIB)}`);
|
|
47
|
+
return parts.length === 0 ? undefined : parts.join(", ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function describeLinux(meminfo: string): string | undefined {
|
|
51
|
+
const kib = (key: string): number | undefined => {
|
|
52
|
+
const value = new RegExp(`^${key}:\\s+(\\d+) kB$`, "mu").exec(meminfo)?.[1];
|
|
53
|
+
return value === undefined ? undefined : Number(value) * KIB;
|
|
54
|
+
};
|
|
55
|
+
const available = kib("MemAvailable");
|
|
56
|
+
const total = kib("MemTotal");
|
|
57
|
+
const swapTotal = kib("SwapTotal");
|
|
58
|
+
const swapFree = kib("SwapFree");
|
|
59
|
+
const parts: string[] = [];
|
|
60
|
+
if (available !== undefined && total !== undefined)
|
|
61
|
+
parts.push(`${formatBytes(available)} of ${formatBytes(total)} memory available`);
|
|
62
|
+
if (swapTotal !== undefined && swapFree !== undefined && swapTotal > 0)
|
|
63
|
+
parts.push(`swap used ${formatBytes(swapTotal - swapFree)}`);
|
|
64
|
+
return parts.length === 0 ? undefined : parts.join(", ");
|
|
65
|
+
}
|
|
@@ -16,8 +16,28 @@ export function describeExit(code: number | null, signal: string | null): string
|
|
|
16
16
|
return code === null ? "an exit with no status" : `exit code ${code}`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** Globals a lost kernel held, as its last result reported them: their names and how many there were in all. */
|
|
20
|
+
export interface LostGlobals {
|
|
21
|
+
readonly names: readonly string[];
|
|
22
|
+
readonly count: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const NAMED_LOST_GLOBALS = 20;
|
|
26
|
+
|
|
27
|
+
export function restartNotice(language: EvalLanguage, reason: string, lost?: LostGlobals): string {
|
|
28
|
+
return `[${language} kernel was restarted after ${reason}; every global is lost${lostPhrase(lost)}]`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The first result of a session's kernel when an earlier kernel of the same session ended with globals defined. */
|
|
32
|
+
export function replacedKernelNotice(language: EvalLanguage, lost: LostGlobals): string {
|
|
33
|
+
return `[${language} kernel is new: this session's previous ${language} kernel ended (session reload, host restart, or resume), so every global it held is gone${lostPhrase(lost)}]`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function lostPhrase(lost: LostGlobals | undefined): string {
|
|
37
|
+
if (lost === undefined || lost.count === 0) return "";
|
|
38
|
+
const named = lost.names.slice(0, NAMED_LOST_GLOBALS);
|
|
39
|
+
const more = lost.count - named.length;
|
|
40
|
+
return `. Lost: ${named.join(", ")}${more > 0 ? ` and ${more} more` : ""}`;
|
|
21
41
|
}
|
|
22
42
|
|
|
23
43
|
export const EVAL_KERNEL_UNAVAILABLE = "eval_kernel_unavailable";
|