@code-yeongyu/senpi-codemode 2026.10.7 → 2026.10.9
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 +38 -0
- package/README.md +67 -5
- package/package.json +5 -4
- package/src/bridge/http-server.ts +9 -1
- package/src/bridge/kernel-tools-protocol.ts +8 -0
- package/src/bridge/protocol.ts +4 -0
- package/src/bridges/eval-virtual-schemas.ts +22 -0
- package/src/environments/editable-check.ts +116 -0
- package/src/environments/install-lock.ts +223 -0
- package/src/environments/py-environment.ts +98 -0
- package/src/environments/py-installer.ts +247 -0
- package/src/environments/python-environments.ts +85 -0
- package/src/environments/revision-store.ts +110 -0
- package/src/extension/bridge-tool-call.ts +2 -0
- package/src/extension/cell-kernel-tools.ts +28 -0
- package/src/extension/kernel-registration.ts +5 -0
- package/src/extension/kernel-replacement.ts +20 -0
- package/src/extension/runtime-factory.ts +12 -0
- package/src/extension/session-manager-proxy.ts +6 -0
- package/src/extension/session-manager.ts +24 -2
- package/src/index.ts +1 -0
- package/src/kernels/jl/prelude.jl +6 -1
- package/src/kernels/js/context-manager.ts +15 -1
- package/src/kernels/js/kernel-tools-errors.js +2 -0
- package/src/kernels/js/kernel-tools-errors.ts +2 -0
- package/src/kernels/js/local-module-loader.ts +9 -4
- package/src/kernels/js/worker-runtime.js +3 -1
- package/src/kernels/js/workpool.js +5 -2
- package/src/kernels/py/kernel-contract.ts +11 -2
- package/src/kernels/py/kernel-tools-host.ts +97 -0
- package/src/kernels/py/kernel.ts +48 -10
- package/src/kernels/py/prelude.py +771 -18
- package/src/kernels/py/process.ts +15 -4
- package/src/kernels/py/startup.ts +58 -20
- package/src/kernels/py/transport.ts +49 -8
- package/src/kernels/rb/workpool.rb +7 -1
- package/src/kernels/sandbox/vendor/pi-codemode/LICENSE +21 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +42 -0
- package/src/kernels/sandbox/vendor/pi-codemode/identifier.ts +14 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +514 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +431 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/protocol.ts +61 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +231 -0
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +185 -0
- package/src/kernels/sandbox/vendor/pi-codemode/wasm.ts +37 -0
- package/src/kernels/shared/cell-source-at-start.ts +18 -0
- package/src/kernels/shared/kernel-tools-pump.ts +6 -0
- package/src/kernels/shared/process-cpu.ts +69 -0
- package/src/kernels/shared/subprocess-queue.ts +1 -0
- package/src/tool/cell-handler.ts +1 -1
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/load-cell.ts +73 -0
- package/src/tool/magic-cell-host.ts +65 -0
- package/src/tool/magic-cells.ts +65 -0
- package/src/tool/run-eval-cell.ts +30 -0
- package/src/tool/types.ts +27 -0
|
@@ -22,6 +22,13 @@ export interface KernelSpawnOptions {
|
|
|
22
22
|
|
|
23
23
|
export type KernelSpawnProcess = (options: KernelSpawnOptions) => KernelChild;
|
|
24
24
|
|
|
25
|
+
/** Signals a kernel's whole process group (addressed by its leader pid); throws like `process.kill`. */
|
|
26
|
+
export type KillProcessGroup = (pid: number, signal: NodeJS.Signals) => void;
|
|
27
|
+
|
|
28
|
+
export const killProcessGroup: KillProcessGroup = (pid, signal) => {
|
|
29
|
+
process.kill(-pid, signal);
|
|
30
|
+
};
|
|
31
|
+
|
|
25
32
|
export class PythonKernelRetirementError extends Error {
|
|
26
33
|
constructor(pid: number | undefined) {
|
|
27
34
|
super(`Python kernel process${pid === undefined ? "" : ` ${pid}`} did not exit after SIGKILL`);
|
|
@@ -95,10 +102,10 @@ export async function waitForExit(child: KernelChild, timeoutMs: number): Promis
|
|
|
95
102
|
// A cell's own subprocess is spawned into the detached kernel's process group. When the
|
|
96
103
|
// leader exits gracefully on close, kill the group so a Popen the cell left running does
|
|
97
104
|
// not outlive the kernel; the group is addressed by the leader pid (== pgid) while members live.
|
|
98
|
-
export function sweepProcessGroup(child: KernelChild): void {
|
|
105
|
+
export function sweepProcessGroup(child: KernelChild, killGroup: KillProcessGroup = killProcessGroup): void {
|
|
99
106
|
if (child.pid === undefined || process.platform === "win32") return;
|
|
100
107
|
try {
|
|
101
|
-
|
|
108
|
+
killGroup(child.pid, "SIGKILL");
|
|
102
109
|
} catch (error) {
|
|
103
110
|
// ESRCH means the group is already gone (the success case). Any other error
|
|
104
111
|
// (for example EPERM on a member we cannot signal) must not fail a graceful close.
|
|
@@ -106,7 +113,11 @@ export function sweepProcessGroup(child: KernelChild): void {
|
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
export async function hardKill(
|
|
116
|
+
export async function hardKill(
|
|
117
|
+
child: KernelChild,
|
|
118
|
+
timeoutMs: number,
|
|
119
|
+
killGroup: KillProcessGroup = killProcessGroup,
|
|
120
|
+
): Promise<void> {
|
|
110
121
|
await new Promise<void>((resolve, reject) => {
|
|
111
122
|
let timer: NodeJS.Timeout | undefined;
|
|
112
123
|
let settled = false;
|
|
@@ -123,7 +134,7 @@ export async function hardKill(child: KernelChild, timeoutMs: number): Promise<v
|
|
|
123
134
|
let signalDelivered = true;
|
|
124
135
|
if (child.pid !== undefined && process.platform !== "win32") {
|
|
125
136
|
try {
|
|
126
|
-
|
|
137
|
+
killGroup(child.pid, "SIGKILL");
|
|
127
138
|
} catch (error) {
|
|
128
139
|
if (!(error instanceof Error)) {
|
|
129
140
|
settle(new Error(String(error)));
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* How long a starting interpreter may stay completely still before startup fails: no new stage, no output
|
|
5
|
+
* line, and no CPU used. A cold, contended interpreter importing the stdlib emits nothing for seconds but
|
|
6
|
+
* keeps the CPU busy, so it never trips; only one that is silent AND idle does. Fresh-cache Windows
|
|
7
|
+
* bootstraps measured p99 5,220 ms of mostly import time (Actions run 36882163342), so 11 s of complete
|
|
8
|
+
* stillness is a stall, not a slow start.
|
|
9
|
+
*/
|
|
6
10
|
export const pythonStartupHangGuardMs = 11_000;
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Total startup budget, a backstop for an interpreter that keeps burning CPU without ever reaching
|
|
14
|
+
* `ready` (a livelock). Over twenty times the slowest measured cold start; never reached by a real one.
|
|
15
|
+
*/
|
|
16
|
+
export const pythonStartupCeilingMs = 120_000;
|
|
17
|
+
|
|
8
18
|
const stages = ["interpreter-launch", "stdlib-imports", "runtime-init", "host-init"] as const;
|
|
9
19
|
export type PythonStartupStage = (typeof stages)[number];
|
|
10
20
|
|
|
@@ -18,19 +28,30 @@ export class PythonKernelStartupError extends Error {
|
|
|
18
28
|
}
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
export interface PythonStartupOptions {
|
|
32
|
+
readonly noProgressMs: number;
|
|
33
|
+
readonly ceilingMs: number;
|
|
34
|
+
readonly failureDetail: () => string;
|
|
35
|
+
/** CPU used so far by the interpreter (any unit that only grows); `undefined` when unreadable. */
|
|
36
|
+
readonly readCpuTime: () => bigint | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
export class PythonStartup {
|
|
23
|
-
readonly #
|
|
24
|
-
readonly #failureDetail: () => string;
|
|
40
|
+
readonly #options: PythonStartupOptions;
|
|
25
41
|
readonly #ready = Promise.withResolvers<void>();
|
|
26
42
|
readonly ready = this.#ready.promise;
|
|
43
|
+
readonly #ceiling: NodeJS.Timeout;
|
|
27
44
|
#timer: NodeJS.Timeout | undefined;
|
|
28
45
|
#stageIndex = 0;
|
|
46
|
+
#lastCpu: bigint | undefined;
|
|
29
47
|
#settled = false;
|
|
30
48
|
|
|
31
|
-
constructor(
|
|
32
|
-
this.#
|
|
33
|
-
this.#
|
|
49
|
+
constructor(options: PythonStartupOptions) {
|
|
50
|
+
this.#options = options;
|
|
51
|
+
this.#ceiling = setTimeout(
|
|
52
|
+
() => this.#fail(`not ready after ${Math.round(options.ceilingMs / 1000)} s`),
|
|
53
|
+
options.ceilingMs,
|
|
54
|
+
);
|
|
34
55
|
this.#arm();
|
|
35
56
|
}
|
|
36
57
|
|
|
@@ -45,31 +66,48 @@ export class PythonStartup {
|
|
|
45
66
|
return stage;
|
|
46
67
|
}
|
|
47
68
|
|
|
69
|
+
activity(): void {
|
|
70
|
+
if (!this.#settled) this.#arm();
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
settle(error?: Error): boolean {
|
|
49
74
|
if (this.#settled) return false;
|
|
50
75
|
this.#settled = true;
|
|
51
76
|
clearTimeout(this.#timer);
|
|
77
|
+
clearTimeout(this.#ceiling);
|
|
52
78
|
if (error) {
|
|
53
|
-
const stage = stages[this.#stageIndex] ?? "interpreter-launch";
|
|
54
79
|
this.#ready.reject(
|
|
55
80
|
error instanceof PythonKernelStartupError
|
|
56
81
|
? error
|
|
57
|
-
: new PythonKernelStartupError(stage, error.message, error),
|
|
82
|
+
: new PythonKernelStartupError(this.#stage(), error.message, error),
|
|
58
83
|
);
|
|
59
84
|
} else this.#ready.resolve();
|
|
60
85
|
return true;
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
#arm(): void {
|
|
89
|
+
this.#lastCpu = this.#options.readCpuTime();
|
|
64
90
|
clearTimeout(this.#timer);
|
|
65
|
-
this.#timer = setTimeout(() =>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
this.#timer = setTimeout(() => this.#expire(), this.#options.noProgressMs);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#expire(): void {
|
|
95
|
+
if (this.#settled) return;
|
|
96
|
+
const cpu = this.#options.readCpuTime();
|
|
97
|
+
// Where CPU can't be read, stage and output are the only signs of life, as before.
|
|
98
|
+
if (cpu !== undefined && cpu !== this.#lastCpu) {
|
|
99
|
+
this.#arm();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.#fail(`no output, stage change or CPU use for ${this.#options.noProgressMs}ms`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#fail(reason: string): void {
|
|
106
|
+
const detail = this.#options.failureDetail().trim();
|
|
107
|
+
this.settle(new PythonKernelStartupError(this.#stage(), `${reason}${detail ? `; ${detail}` : ""}`));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#stage(): PythonStartupStage {
|
|
111
|
+
return stages[this.#stageIndex] ?? "interpreter-launch";
|
|
74
112
|
}
|
|
75
113
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../../bridge/protocol.ts";
|
|
12
12
|
import { applySessionEnvironment, type SessionEnvironment } from "../session-env.ts";
|
|
13
13
|
import type { KernelPreludePlan } from "../shared/kernel-prelude-plan.ts";
|
|
14
|
+
import { readKernelCpuTime } from "../shared/process-cpu.ts";
|
|
14
15
|
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
15
16
|
import {
|
|
16
17
|
defaultSpawn,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type KernelChild,
|
|
19
20
|
type KernelSpawnOptions,
|
|
20
21
|
type KernelSpawnProcess,
|
|
22
|
+
type KillProcessGroup,
|
|
21
23
|
numberOrNull,
|
|
22
24
|
signalOrNull,
|
|
23
25
|
splitCommand,
|
|
@@ -33,6 +35,9 @@ export interface PythonTransportRunInput {
|
|
|
33
35
|
readonly code: string;
|
|
34
36
|
readonly timeoutMs?: number;
|
|
35
37
|
readonly preludePlan?: KernelPreludePlan;
|
|
38
|
+
readonly envRoot?: () => string;
|
|
39
|
+
readonly sourceFile?: string;
|
|
40
|
+
readonly bridgeCellToken?: string;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
export interface PythonTransportOptions {
|
|
@@ -44,8 +49,13 @@ export interface PythonTransportOptions {
|
|
|
44
49
|
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
45
50
|
readonly sessionEnv?: SessionEnvironment;
|
|
46
51
|
readonly startupTimeoutMs: number;
|
|
52
|
+
readonly startupCeilingMs: number;
|
|
53
|
+
readonly readCpuTime?: (pid: number | undefined) => bigint | undefined;
|
|
54
|
+
readonly killProcessGroup?: KillProcessGroup;
|
|
47
55
|
readonly onStartupProgress?: (stage: PythonStartupStage) => void;
|
|
48
56
|
readonly memory?: KernelMemoryThresholds;
|
|
57
|
+
/** Kernel-tool descriptors carry this; a restarted interpreter gets a new one, so old descriptors go stale. */
|
|
58
|
+
readonly kernelGeneration?: number;
|
|
49
59
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
50
60
|
readonly spawnProcess?: KernelSpawnProcess;
|
|
51
61
|
readonly isOwned: () => boolean;
|
|
@@ -140,12 +150,26 @@ export class PythonKernelTransport {
|
|
|
140
150
|
return transport;
|
|
141
151
|
}
|
|
142
152
|
|
|
153
|
+
/** Kernel-tool frames for the runner's control reader (served even while a cell runs). */
|
|
154
|
+
post(message: HostToKernelMessage): void {
|
|
155
|
+
if (this.#active && !this.#exited) this.#write(message);
|
|
156
|
+
}
|
|
157
|
+
|
|
143
158
|
run(input: PythonTransportRunInput): void {
|
|
144
159
|
const preludes = input.preludePlan && {
|
|
145
160
|
install: input.preludePlan.install.map(({ exports, python }) => ({ exports: [...exports], python })),
|
|
146
161
|
remove: [...input.preludePlan.remove],
|
|
147
162
|
};
|
|
148
|
-
this.#write({
|
|
163
|
+
this.#write({
|
|
164
|
+
type: "run",
|
|
165
|
+
cellId: input.cellId,
|
|
166
|
+
code: input.code,
|
|
167
|
+
timeoutMs: input.timeoutMs,
|
|
168
|
+
preludes,
|
|
169
|
+
...(input.envRoot === undefined ? {} : { envRoot: input.envRoot() }),
|
|
170
|
+
...(input.sourceFile === undefined ? {} : { sourceFile: input.sourceFile }),
|
|
171
|
+
...(input.bridgeCellToken === undefined ? {} : { bridgeCellToken: input.bridgeCellToken }),
|
|
172
|
+
});
|
|
149
173
|
}
|
|
150
174
|
|
|
151
175
|
interrupt(reason: string): void {
|
|
@@ -166,7 +190,7 @@ export class PythonKernelTransport {
|
|
|
166
190
|
return;
|
|
167
191
|
}
|
|
168
192
|
if (!this.#active) {
|
|
169
|
-
await hardKill(this.#child, hardKillWaitMs);
|
|
193
|
+
await hardKill(this.#child, hardKillWaitMs, this.#options.killProcessGroup);
|
|
170
194
|
return;
|
|
171
195
|
}
|
|
172
196
|
this.#active = false;
|
|
@@ -178,15 +202,15 @@ export class PythonKernelTransport {
|
|
|
178
202
|
}
|
|
179
203
|
// hardKill kills the whole group; a graceful leader exit does not, so sweep it
|
|
180
204
|
// to retire any subprocess the cell left running in the kernel's process group.
|
|
181
|
-
if (await exited) sweepProcessGroup(this.#child);
|
|
182
|
-
else await hardKill(this.#child, hardKillWaitMs);
|
|
205
|
+
if (await exited) sweepProcessGroup(this.#child, this.#options.killProcessGroup);
|
|
206
|
+
else await hardKill(this.#child, hardKillWaitMs, this.#options.killProcessGroup);
|
|
183
207
|
}
|
|
184
208
|
|
|
185
209
|
retire(): Promise<void> {
|
|
186
210
|
if (this.#exited || this.#isGone) return Promise.resolve();
|
|
187
211
|
if (this.#retirement) return this.#retirement;
|
|
188
212
|
this.#active = false;
|
|
189
|
-
const retirement = hardKill(this.#child, hardKillWaitMs).finally(() => {
|
|
213
|
+
const retirement = hardKill(this.#child, hardKillWaitMs, this.#options.killProcessGroup).finally(() => {
|
|
190
214
|
this.#detachListeners();
|
|
191
215
|
if (this.#retirement === retirement) this.#retirement = null;
|
|
192
216
|
});
|
|
@@ -195,7 +219,16 @@ export class PythonKernelTransport {
|
|
|
195
219
|
}
|
|
196
220
|
|
|
197
221
|
async #initialize(): Promise<void> {
|
|
198
|
-
const
|
|
222
|
+
const pid = this.#child.pid;
|
|
223
|
+
const startup = new PythonStartup({
|
|
224
|
+
noProgressMs: this.#options.startupTimeoutMs,
|
|
225
|
+
ceilingMs: this.#options.startupCeilingMs,
|
|
226
|
+
failureDetail: () => this.#stderrTail,
|
|
227
|
+
readCpuTime: () => {
|
|
228
|
+
if (this.#options.readCpuTime) return this.#options.readCpuTime(pid);
|
|
229
|
+
return pid === undefined ? undefined : readKernelCpuTime(pid);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
199
232
|
this.#startup = startup;
|
|
200
233
|
const onStdout = (chunk: unknown) => this.#onStdout(String(chunk));
|
|
201
234
|
const onStderr = (chunk: unknown) => this.#onStderr(String(chunk));
|
|
@@ -211,8 +244,14 @@ export class PythonKernelTransport {
|
|
|
211
244
|
this.#child.stderr.on("data", onStderr);
|
|
212
245
|
this.#child.on("error", onError);
|
|
213
246
|
this.#child.on("exit", onExit);
|
|
214
|
-
const { sessionId, connection, memory } = this.#options;
|
|
215
|
-
this.#write({
|
|
247
|
+
const { sessionId, connection, memory, kernelGeneration } = this.#options;
|
|
248
|
+
this.#write({
|
|
249
|
+
type: "init",
|
|
250
|
+
sessionId,
|
|
251
|
+
connection,
|
|
252
|
+
...(memory === undefined ? {} : { memory }),
|
|
253
|
+
...(kernelGeneration === undefined ? {} : { kernelGeneration }),
|
|
254
|
+
});
|
|
216
255
|
await startup.ready;
|
|
217
256
|
}
|
|
218
257
|
|
|
@@ -234,6 +273,7 @@ export class PythonKernelTransport {
|
|
|
234
273
|
|
|
235
274
|
#onStderr(chunk: string): void {
|
|
236
275
|
if (!this.#active) return;
|
|
276
|
+
this.#startup?.activity();
|
|
237
277
|
this.#stderrTail = `${this.#stderrTail}${chunk}`.slice(-4_000);
|
|
238
278
|
this.#options.onMessage?.({ type: "text", stream: "stderr", data: chunk });
|
|
239
279
|
}
|
|
@@ -251,6 +291,7 @@ export class PythonKernelTransport {
|
|
|
251
291
|
if (stage !== undefined) this.#options.onStartupProgress?.(stage);
|
|
252
292
|
return;
|
|
253
293
|
}
|
|
294
|
+
if (message.type === "text") this.#startup?.activity();
|
|
254
295
|
if (message.type === "ready") this.#settleStartup();
|
|
255
296
|
else if (message.type === "init-failed") this.#settleStartup(new Error(message.error.message));
|
|
256
297
|
else if (message.type === "result") this.#options.onResult(this, message);
|
|
@@ -23,9 +23,15 @@ class SenpiWorkpool
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def workpool(agent, name, mode: nil)
|
|
26
|
+
def workpool(agent, name, mode: nil, tools: nil)
|
|
27
27
|
args = { "op" => "create", "agent" => agent, "name" => name }
|
|
28
28
|
args["mode"] = mode unless mode.nil?
|
|
29
|
+
unless tools.nil?
|
|
30
|
+
unless tools.is_a?(Array) && tools.all? { |name| name.is_a?(String) }
|
|
31
|
+
raise SenpiBridgeError.new("workpool(tools: ...) takes an array of tool names; got #{tools.class}", "invalid_tools")
|
|
32
|
+
end
|
|
33
|
+
args["tools"] = tools.dup
|
|
34
|
+
end
|
|
29
35
|
result = __senpi_workpool_call(args)
|
|
30
36
|
details = result["details"]
|
|
31
37
|
if details.is_a?(Hash)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mario Zechner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Vendored: @earendil-works/pi-codemode 1.0.1
|
|
2
|
+
|
|
3
|
+
- Upstream: https://github.com/earendil-works/pi, directory `packages/codemode/src`
|
|
4
|
+
- Tag `v1.0.1`, commit `a7229ddc21810d6245105978033b7df645ecc2f7` (`packages/codemode/package.json` version 1.0.1)
|
|
5
|
+
- License: MIT, upstream `LICENSE` copied unchanged beside this file.
|
|
6
|
+
- Why vendored: the published package ships compiled `dist` only, and senpi needs three host changes that are not in any release (listed below once they land). Vendoring the source keeps them in this repository, so a registry consumer receives them.
|
|
7
|
+
|
|
8
|
+
## Provenance
|
|
9
|
+
|
|
10
|
+
Building `packages/codemode` at that commit with its pinned compiler (`typescript` 7.0.2, `tsc -p tsconfig.build.json`) reproduces all 40 files of the published npm `@earendil-works/pi-codemode@1.0.1` `dist` byte for byte, so this source is the released code.
|
|
11
|
+
|
|
12
|
+
Each file below starts with a two-line attribution header. `wasm.ts` and `identifier.ts` are otherwise the upstream file unchanged. The other five carry the local changes listed below, and nothing else: outside the `senpi-change` blocks and the lines marked `// senpi-change`, every line is upstream's, in upstream's order. The SHA-256 is of the upstream file.
|
|
13
|
+
|
|
14
|
+
| file | upstream SHA-256 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `runtime/host.ts` | `b81e1bb07f3cd7e612c5ed03b6d23929acf3738be764c0741b85306d8221c522` |
|
|
17
|
+
| `runtime/worker.ts` | `f2e4854de6c9713390148a7d02b35e753065aca42c759ca14676f90f937f0a8a` |
|
|
18
|
+
| `runtime/prelude-source.ts` | `bd544106d5bd36543be4e5fc21128c9cd46b26856fb0b17931250b955f93c24a` |
|
|
19
|
+
| `runtime/protocol.ts` | `b5150df8e6c743f99a94f13cf6f4ae7515768bb9e6305e1a7e4dacd20e4dc6ff` |
|
|
20
|
+
| `types.ts` | `a54fd8d97468bedd16cc1aae6d89648581da90bd55f65bb0b116c947893b5920` |
|
|
21
|
+
| `wasm.ts` | `0785024745568c86611b00a82f2bc8f70fc1559eff7200e64a449ebbeeeebb94` |
|
|
22
|
+
| `identifier.ts` | `c920a297e79186edc61348906950b639e8d67602122f165fb98012fd32be2f11` |
|
|
23
|
+
|
|
24
|
+
Only these seven files are vendored: the closure of `runtime/host.ts` (the sandbox host). `index.ts`, `source.ts` and `declarations.ts` are not used.
|
|
25
|
+
|
|
26
|
+
## Local changes
|
|
27
|
+
|
|
28
|
+
Every change is marked, in one of two ways, so a sync can find each one:
|
|
29
|
+
|
|
30
|
+
- a block between `// senpi-change begin: <topic>` and `// senpi-change end`, which adds code or replaces the upstream lines it stands in place of (for example `types.ts`'s `CodemodeResult`, and the end of `finish()` in `runtime/host.ts`);
|
|
31
|
+
- a single upstream line edited in place, ending in `// senpi-change` (an extra import name, the `"output-frame"` message type in `isWorkerToHostMessage`, the prelude's `optionsJson` parameter and its stream-mode output-cap condition).
|
|
32
|
+
|
|
33
|
+
A sync must re-apply both kinds: dropping a marked line (for example `"output-frame"` in the protocol guard) would make the host ignore every frame. Each one is an option whose default is upstream's behaviour: with the defaults, the ported upstream suite (`test/sandbox/vendor/upstream-sandbox.test.ts`, 47 cases, only its imports changed) passes unchanged.
|
|
34
|
+
|
|
35
|
+
1. **Output streaming** (`output: "stream"`, `onOutputFrame`, `windowBytes`, `frameBytes`; `types.ts`, `runtime/protocol.ts`, `runtime/worker.ts`, `runtime/host.ts`, `runtime/prelude-source.ts`). Upstream keeps every output item until the script settles, which is why it caps output at 16 Mi characters. In stream mode:
|
|
36
|
+
- The host keeps nothing; `result.output` stays empty and `result.streamed` reports frames and the peak bytes in flight.
|
|
37
|
+
- The worker splits each item into frames of at most `frameBytes` (default 64 KiB), never splitting a surrogate pair, tagged with item id, sequence and a final flag.
|
|
38
|
+
- It sends a frame only after taking that many bytes from a shared credit counter (default window 256 KiB). It sleeps on the counter with `Atomics.wait` while the window is full, and re-checks the interrupt flag each time it wakes.
|
|
39
|
+
- The host hands frames to the consumer one at a time, in order, and returns their credit only once the consumer has finished. The output cap does not apply, because memory is bounded by the window.
|
|
40
|
+
- A normal finish settles only after every frame is consumed. An abort or timeout settles at once and drops frames still queued. The timer and the abort signal stay armed while a finished run's last frames drain, so a late abort, timeout or `close()` abandons them too (the run settles with that error): a consumer that never returns cannot keep a run alive.
|
|
41
|
+
- `frameBytes` and `windowBytes` must be at least 4, so a frame can always hold a whole surrogate pair.
|
|
42
|
+
2. **Builtin store policy** (`builtins: { store: "collect" | "reject" }`; `types.ts`, `runtime/protocol.ts`, `runtime/worker.ts`, `runtime/host.ts`, `runtime/prelude-source.ts`). With `"reject"`, `load()` returns `undefined` and `store()` throws `CodemodeStoreDisabledError`. The names stay defined, so scripts written for upstream fail with a clear message instead of a ReferenceError.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Vendored from https://github.com/earendil-works/pi (packages/codemode/src) at v1.0.1, commit a7229ddc21810d6245105978033b7df645ecc2f7.
|
|
2
|
+
// MIT license; see LICENSE beside this file. Local changes are listed in VENDORED.md.
|
|
3
|
+
/**
|
|
4
|
+
* The identifier a script uses for a tool: characters that are not valid in a JavaScript
|
|
5
|
+
* identifier become `_`. `mcp__docs__search` stays as is, `my-tool` becomes `my_tool`.
|
|
6
|
+
*/
|
|
7
|
+
export function toCodemodeIdentifier(name: string): string {
|
|
8
|
+
let identifier = "";
|
|
9
|
+
for (const char of name) {
|
|
10
|
+
const valid = identifier === "" ? /^[A-Za-z_$]$/.test(char) : /^[A-Za-z0-9_$]$/.test(char);
|
|
11
|
+
identifier += valid ? char : "_";
|
|
12
|
+
}
|
|
13
|
+
return identifier === "" ? "_" : identifier;
|
|
14
|
+
}
|