@code-yeongyu/senpi-codemode 2026.9.5 → 2026.9.7-2
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 +74 -0
- package/README.md +34 -5
- package/package.json +4 -4
- package/src/bridge/protocol.ts +1 -0
- package/src/bridge/reserved.ts +2 -0
- package/src/extension/runtime-factory.ts +3 -0
- package/src/extension/session-manager.ts +7 -0
- package/src/kernels/AGENTS.md +7 -0
- package/src/kernels/jl/kernel.ts +6 -2
- package/src/kernels/js/context-manager.ts +73 -129
- package/src/kernels/js/inline-worker.ts +2 -2
- package/src/kernels/js/interrupt-bounds.ts +66 -0
- package/src/kernels/js/kernel-contract.ts +3 -0
- package/src/kernels/js/run-queue.ts +18 -3
- package/src/kernels/js/worker-core.js +59 -0
- package/src/kernels/js/worker-indirect-eval.js +10 -6
- package/src/kernels/js/worker-runtime.js +16 -0
- package/src/kernels/js/worker-shell-capture.d.ts +25 -0
- package/src/kernels/js/worker-shell-capture.js +70 -8
- package/src/kernels/js/worker-slot.ts +106 -0
- package/src/kernels/js/worker-startup.ts +70 -0
- package/src/kernels/py/kernel-contract.ts +3 -0
- package/src/kernels/py/transport.ts +11 -3
- package/src/kernels/rb/kernel.ts +6 -2
- package/src/kernels/session-env.ts +56 -0
- package/src/kernels/shared/runtime-asset.ts +40 -14
- package/src/kernels/shared/subprocess-contract.ts +3 -0
- package/src/kernels/shared/subprocess-kernel.ts +9 -1
- package/src/output/output-meta.ts +26 -2
- package/src/prompt/eval-prompt.ts +10 -5
- package/src/tool/cell-execution.ts +9 -11
- package/src/tool/detached-cell-contract.ts +45 -0
- package/src/tool/detached-cell-manager.ts +43 -71
- package/src/tool/detached-cell-notification.ts +4 -5
- package/src/tool/detached-cell-snapshot.ts +2 -0
- package/src/tool/detached-cell-status.ts +30 -0
- package/src/tool/detached-eval-result.ts +1 -0
- package/src/tool/detached-notification-queue.ts +2 -2
- package/src/tool/image.ts +12 -3
- package/src/tool/interrupt-note.ts +29 -15
- package/src/tool/types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,80 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.9.7-2] - 2026-09-07
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- The JS kernel's shell capture now pins the worker's environment view for `Bun.spawnSync` as well as `Bun.spawn`, so a cell calling it without an explicit `env` sees the session's `PI_*` values instead of the inherited OS environ.
|
|
26
|
+
- Eval kernels and every child they spawn now see the active session's `PI_*` environment (`PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, `PI_REASONING_LEVEL`) exactly as bash-tool children do: inherited `PI_*` values are dropped before the session values are applied, so subprocesses such as `omo-agent-toolkit ulw-loop` resolve the same session as the `bash` tool instead of a cwd-global one.
|
|
27
|
+
- JavaScript eval cells no longer lose their completion value when a nested function, callback, or try/catch helper contains `return`: the cell wrapper now skips last-expression capture only for a genuine top-level `return`, and a property named `return` no longer primes the statement scanner as the keyword (#1439).
|
|
28
|
+
- Eval output truncation notices now name the real cause: a width-clamped line reports `N line(s) clamped to M columns (… dropped)`, a byte-capped tail reports the actual cap, and a notice never presents the output's own size as a limit.
|
|
29
|
+
|
|
30
|
+
### Removed
|
|
31
|
+
|
|
32
|
+
## [2026.9.7] - 2026-09-07
|
|
33
|
+
|
|
34
|
+
### Breaking Changes
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
44
|
+
## [2026.9.6] - 2026-09-06
|
|
45
|
+
|
|
46
|
+
### Breaking Changes
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- The Bun eval description now tells the model to shell out through `Bun.$` or `Bun.spawn` and never `Bun.spawnSync`, because a synchronous child blocks the worker and a stop or timeout then loses every variable.
|
|
53
|
+
- JavaScript eval cells now interrupt cooperatively: `stop` and kernel timeouts first ask the worker to settle the cell (pending bridge `tool.*` calls are rejected, `Bun.spawn` children are killed) and keep the worker VM and its globals when the cell settles within a 2 s grace; only an unsettled cell restarts the worker.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- `eval({ action: "stop" })` no longer hangs when the JavaScript worker is blocked in a synchronous call such as `Bun.spawnSync`: worker termination is bounded by a 3 s deadline, a fresh worker replaces the blocked one, and the cell output names the blocked synchronous call.
|
|
58
|
+
- `Bun.$` commands run from a JavaScript cell no longer inherit the TUI's terminal as stdin (a stdin reader such as `cat`, an ssh or git credential prompt, or a keychain prompt blocked the cell forever); the shell wrapper isolates stdin while a cell is active without changing output, exit codes, `cwd`, `env`, or explicit stdin redirects.
|
|
59
|
+
- Stop results and detached-cell completion notifications report the real interrupt outcome (variables preserved, worker restarted, or outcome unknown) instead of a hardcoded per-language note.
|
|
60
|
+
|
|
61
|
+
### Removed
|
|
62
|
+
|
|
63
|
+
## [2026.9.5-3] - 2026-09-05
|
|
64
|
+
|
|
65
|
+
### Breaking Changes
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
### Removed
|
|
74
|
+
|
|
75
|
+
## [2026.9.5-2] - 2026-09-05
|
|
76
|
+
|
|
77
|
+
### Breaking Changes
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- The GPT eval dialect now routes a wait or a long run through `tool.monitor` inside the cell (the subscription line precedes the detach note, and the `## Tool Guidelines` line says so when `monitor` is reachable), so a GPT model no longer reads "long cells detach" as the way to wait on a `--watch`.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
### Removed
|
|
88
|
+
|
|
15
89
|
## [2026.9.5] - 2026-09-05
|
|
16
90
|
|
|
17
91
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -54,6 +54,19 @@ task-tool names are known.
|
|
|
54
54
|
A missing optional interpreter removes that language from the session's `eval`
|
|
55
55
|
schema; it is not an installation failure.
|
|
56
56
|
|
|
57
|
+
### Session environment
|
|
58
|
+
|
|
59
|
+
Every kernel starts with the active session's `PI_*` environment — `PI_SESSION_ID`,
|
|
60
|
+
`PI_SESSION_FILE` (when the session is persistent), `PI_PROVIDER`, `PI_MODEL`, and
|
|
61
|
+
`PI_REASONING_LEVEL` (when set) — resolved at session start, mirroring the bash tool's
|
|
62
|
+
session environment contract. The values are visible to `env()`/`process.env`/`os.environ`
|
|
63
|
+
inside cells and are inherited by every child process a cell spawns
|
|
64
|
+
(`Bun.$`, `Bun.spawn`, `child_process`, `subprocess`, ...). Inherited `PI_*` values from
|
|
65
|
+
the launching environment are dropped first, so a child spawned from a cell sees exactly
|
|
66
|
+
what a child spawned from the bash tool sees. The values snapshot at kernel start, so a
|
|
67
|
+
mid-session model switch updates the bash tool's next command but not already-running
|
|
68
|
+
kernels; a new session starts fresh kernels with fresh values.
|
|
69
|
+
|
|
57
70
|
## Settings
|
|
58
71
|
|
|
59
72
|
Configuration is loaded in this order:
|
|
@@ -116,7 +129,7 @@ options object and asynchronous helpers are `await`-able.
|
|
|
116
129
|
| `print(value, ...)` | Emits text output. |
|
|
117
130
|
| `read(path, offset?, limit?)` | Reads text with 1-indexed line slicing. `local://` paths resolve under the session artifact root. |
|
|
118
131
|
| `write(path, content)` | Creates parent directories and writes text. `local://` paths persist in the session artifact root. |
|
|
119
|
-
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. |
|
|
132
|
+
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. Includes the session's `PI_*` values (see [Session environment](#session-environment)). |
|
|
120
133
|
| `tool.<name>(args)` | Invokes an active Senpi tool through the normal `pi.executeTool` pipeline and returns `{ text, images?, details?, hasError? }` in every kernel; image blocks arrive as `images[i] = { mimeType, dataBase64 }`. |
|
|
121
134
|
| `tool_schema(name?)` | Returns a tool's parameter schema without calling it; omit `name` to list tool names. |
|
|
122
135
|
| `completion(prompt, model?, system?, schema?)` | Requests a one-shot host completion; `schema` asks the host to parse structured output. |
|
|
@@ -166,10 +179,26 @@ when the call had no summary), clearing as soon as the last detached cell settle
|
|
|
166
179
|
|
|
167
180
|
Use `eval({ action: "peek", cell_id })` for its state and buffered output, or
|
|
168
181
|
`eval({ action: "stop", cell_id })` to cancel it. Python stop interrupts the
|
|
169
|
-
existing kernel and preserves variables. JavaScript stop
|
|
170
|
-
worker
|
|
171
|
-
|
|
172
|
-
|
|
182
|
+
existing kernel and preserves variables. JavaScript stop is cooperative first:
|
|
183
|
+
the worker rejects the cell's pending bridge `tool.*` calls and kills the
|
|
184
|
+
`Bun.spawn` children it started, and a cell that settles within the 2 s grace
|
|
185
|
+
keeps the worker and every global. Only a cell that stays unsettled (a
|
|
186
|
+
never-resolving promise, an un-abortable `fetch`, a `Bun.$` command) costs the
|
|
187
|
+
worker VM. A worker blocked in a synchronous call (`Bun.spawnSync`,
|
|
188
|
+
`child_process.spawnSync`) cannot be stopped at all; after a 3 s termination
|
|
189
|
+
deadline a fresh worker replaces it, the cell output gains a stderr line naming
|
|
190
|
+
the blocked synchronous call, and the blocked call keeps running until it
|
|
191
|
+
returns. Kernel-level timeouts follow the same path. Stop results and detached
|
|
192
|
+
completion messages report the real outcome - variables preserved, worker
|
|
193
|
+
restarted, or outcome unknown - never a per-language assumption; oversized
|
|
194
|
+
buffered output is written under the session local root and referenced as
|
|
195
|
+
`local://…`.
|
|
196
|
+
|
|
197
|
+
Commands a cell runs through `Bun.$` never read the host's terminal: the worker
|
|
198
|
+
thread shares the TUI's stdin, so the shell wrapper hands every template an
|
|
199
|
+
empty pipe (`true | ( … )`) while a cell is active. Output, exit codes, `cwd`,
|
|
200
|
+
`env`, and explicit `< ${input}` redirects are unchanged; `Bun.spawn` and
|
|
201
|
+
`Bun.spawnSync` already default stdin to `/dev/null`.
|
|
173
202
|
|
|
174
203
|
## Output and artifacts
|
|
175
204
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.7-2",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@babel/parser": "8.0.4",
|
|
33
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.7-2",
|
|
34
34
|
"typebox": "1.3.18"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.9.7-2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.9.7-2"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
package/src/bridge/protocol.ts
CHANGED
|
@@ -26,6 +26,7 @@ const hostToKernelMessageSchema = Type.Union([
|
|
|
26
26
|
type: Type.Literal("init"),
|
|
27
27
|
sessionId: Type.String({ minLength: 1 }),
|
|
28
28
|
connection: connectionConfigSchema,
|
|
29
|
+
sessionEnv: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
29
30
|
}),
|
|
30
31
|
Type.Object({
|
|
31
32
|
type: Type.Literal("run"),
|
package/src/bridge/reserved.ts
CHANGED
|
@@ -9,3 +9,5 @@ export const RESERVED_SCHEMA_TOOL = "__schema__" as const;
|
|
|
9
9
|
export const TIMEOUT_PAUSE_OP = "timeout-pause" as const;
|
|
10
10
|
/** Canonical oh-my-pi eval-timeout resume operation. */
|
|
11
11
|
export const TIMEOUT_RESUME_OP = "timeout-resume" as const;
|
|
12
|
+
/** Status op the JS worker emits the moment it receives `interrupt`; proves its event loop is not blocked. */
|
|
13
|
+
export const INTERRUPT_ACK_OP = "interrupt-ack" as const;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getInterpreterAvailability,
|
|
14
14
|
type InterpreterAvailability,
|
|
15
15
|
} from "../interpreters/detect.ts";
|
|
16
|
+
import { sessionEnvironmentFrom } from "../kernels/session-env.ts";
|
|
16
17
|
import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
|
|
17
18
|
import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
|
|
18
19
|
import { jsRuntimeInfo, runtimesFromAvailability } from "./runtime-info.ts";
|
|
@@ -66,11 +67,13 @@ export async function createRuntime(
|
|
|
66
67
|
const executeTool = createExecuteTool(pi, activeTools);
|
|
67
68
|
const create = options.createSessionManager ?? createCodemodeSessionManager;
|
|
68
69
|
const sessionId = sessionIdFrom(event);
|
|
70
|
+
const sessionEnv = sessionEnvironmentFrom(ctx);
|
|
69
71
|
const configuredPoolWidth = settings.parallelPoolWidth;
|
|
70
72
|
const parallelPoolWidth = Number.isFinite(configuredPoolWidth) ? Math.max(1, Math.trunc(configuredPoolWidth)) : 1;
|
|
71
73
|
const manager = await create({
|
|
72
74
|
sessionId,
|
|
73
75
|
cwd: ctx.cwd,
|
|
76
|
+
sessionEnv,
|
|
74
77
|
settings,
|
|
75
78
|
availability,
|
|
76
79
|
artifactsDir: artifacts.dir,
|
|
@@ -11,6 +11,7 @@ import { JuliaKernel } from "../kernels/jl/kernel.ts";
|
|
|
11
11
|
import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
|
|
12
12
|
import { PythonKernel } from "../kernels/py/kernel.ts";
|
|
13
13
|
import { RubyKernel } from "../kernels/rb/kernel.ts";
|
|
14
|
+
import type { SessionEnvironment } from "../kernels/session-env.ts";
|
|
14
15
|
import { marshalToolResult } from "../tool/image.ts";
|
|
15
16
|
import type { EvalKernel, EvalKernelManager, EvalLanguage, ExecuteTool } from "../tool/types.ts";
|
|
16
17
|
|
|
@@ -40,6 +41,8 @@ export interface CreateCodemodeSessionManagerOptions {
|
|
|
40
41
|
readonly localRoots?: Readonly<Record<string, string>>;
|
|
41
42
|
/** Session-adjacent directory used for persisted eval artifacts. */
|
|
42
43
|
readonly artifactsDir?: string;
|
|
44
|
+
/** Per-session PI_* values exposed to every kernel and the children it spawns. */
|
|
45
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
43
46
|
readonly executeTool: ExecuteTool;
|
|
44
47
|
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
45
48
|
readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
@@ -212,6 +215,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
212
215
|
cwd: this.#options.cwd,
|
|
213
216
|
parallelPoolWidth,
|
|
214
217
|
onMessage,
|
|
218
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
215
219
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
216
220
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
217
221
|
});
|
|
@@ -230,6 +234,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
230
234
|
interpreterPath: detected.path,
|
|
231
235
|
sessionId: this.#options.sessionId,
|
|
232
236
|
cwd: this.#options.cwd,
|
|
237
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
233
238
|
connection,
|
|
234
239
|
onMessage,
|
|
235
240
|
});
|
|
@@ -239,6 +244,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
239
244
|
command: detected.path,
|
|
240
245
|
sessionId: this.#options.sessionId,
|
|
241
246
|
cwd: this.#options.cwd,
|
|
247
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
242
248
|
connection,
|
|
243
249
|
onMessage,
|
|
244
250
|
});
|
|
@@ -247,6 +253,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
247
253
|
command: detected.path,
|
|
248
254
|
sessionId: this.#options.sessionId,
|
|
249
255
|
cwd: this.#options.cwd,
|
|
256
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
250
257
|
connection,
|
|
251
258
|
onMessage,
|
|
252
259
|
});
|
package/src/kernels/AGENTS.md
CHANGED
|
@@ -15,6 +15,7 @@ runner/prelude assets).
|
|
|
15
15
|
| Ruby kernel | `rb/kernel.ts` + `rb/prelude.rb`, `rb/runner.rb` |
|
|
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
|
+
| Session environment | `session-env.ts` (PI_* contract shared by all kernels; mirrors the core bash tool) |
|
|
18
19
|
|
|
19
20
|
## CONVENTIONS
|
|
20
21
|
|
|
@@ -29,6 +30,12 @@ runner/prelude assets).
|
|
|
29
30
|
framed subprocesses through `shared/`.
|
|
30
31
|
- Subprocess retirement/restart, worker recovery, timeout, and interrupt
|
|
31
32
|
semantics live here, never in the tool layer.
|
|
33
|
+
- Every kernel exposes the active session's `PI_*` environment (`session-env.ts`):
|
|
34
|
+
inherited values are deleted before the session's values are applied, so any
|
|
35
|
+
child spawned from a cell sees the same session environment a bash-tool child
|
|
36
|
+
sees. The JS worker applies it at init (`worker-core.js`; shell capture pins
|
|
37
|
+
the env view under Bun because `delete process.env.X` does not unsetenv),
|
|
38
|
+
and py/rb/jl spawn with it merged into the interpreter environment.
|
|
32
39
|
|
|
33
40
|
## ANTI-PATTERNS
|
|
34
41
|
|
package/src/kernels/jl/kernel.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
|
-
import
|
|
3
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
4
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
4
5
|
import { SubprocessKernel, type SubprocessSpawn } from "../shared/subprocess-kernel.ts";
|
|
5
6
|
|
|
6
7
|
export interface JuliaKernelStartOptions {
|
|
7
8
|
readonly cwd: string;
|
|
8
9
|
readonly sessionId: string;
|
|
9
10
|
readonly connection: BridgeConnectionConfig;
|
|
11
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
12
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
10
13
|
readonly command?: string;
|
|
11
14
|
readonly spawn?: SubprocessSpawn;
|
|
12
15
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
@@ -17,7 +20,7 @@ export interface JuliaRunnerPathOptions extends CodemodeRuntimeAssetEnvironment
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function resolveJuliaRunnerPath(options: JuliaRunnerPathOptions = {}): string {
|
|
20
|
-
return
|
|
23
|
+
return requireCodemodeRuntimeAsset(
|
|
21
24
|
options.localPath ?? join(import.meta.dirname, "runner.jl"),
|
|
22
25
|
join("kernels", "jl", "runner.jl"),
|
|
23
26
|
options,
|
|
@@ -42,6 +45,7 @@ export class JuliaKernel extends SubprocessKernel {
|
|
|
42
45
|
],
|
|
43
46
|
cwd: options.cwd,
|
|
44
47
|
sessionId: options.sessionId,
|
|
48
|
+
sessionEnv: options.sessionEnv,
|
|
45
49
|
connection: options.connection,
|
|
46
50
|
spawn: options.spawn,
|
|
47
51
|
onMessage: options.onMessage,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { dirname, join } from "node:path";
|
|
2
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
1
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import { INTERRUPT_ACK_OP } from "../../bridge/reserved.ts";
|
|
4
3
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
5
|
-
import {
|
|
6
|
-
import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
|
|
4
|
+
import { abandonedWorkerNote, awaitCooperativeSettlement, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
7
5
|
import {
|
|
8
6
|
assertJavaScriptKernelOpen,
|
|
9
7
|
type JavaScriptKernelMode,
|
|
@@ -12,31 +10,20 @@ import {
|
|
|
12
10
|
type ResultMessage,
|
|
13
11
|
type ToolCallMessage,
|
|
14
12
|
} from "./kernel-contract.ts";
|
|
15
|
-
import { type JavaScriptKernelOptions, LocalModuleLoader
|
|
13
|
+
import { type JavaScriptKernelOptions, LocalModuleLoader } from "./local-module-loader.ts";
|
|
16
14
|
import { JavaScriptRunQueue, type PendingJavaScriptRun, stoppedResult } from "./run-queue.ts";
|
|
17
|
-
import { bridgeError,
|
|
15
|
+
import { bridgeError, WorkerStartupCancelledError } from "./worker-host.ts";
|
|
16
|
+
import { WorkerSlot } from "./worker-slot.ts";
|
|
18
17
|
|
|
19
18
|
export { JavaScriptKernelClosedError, type JavaScriptKernelMode, type JavaScriptRunInput } from "./kernel-contract.ts";
|
|
20
19
|
export type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
21
|
-
|
|
22
|
-
export interface JavaScriptWorkerEntryUrlOptions extends CodemodeRuntimeAssetEnvironment {
|
|
23
|
-
readonly localPath?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function resolveJsWorkerEntryUrl(options: JavaScriptWorkerEntryUrlOptions = {}): URL {
|
|
27
|
-
const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "worker-entry.js");
|
|
28
|
-
return pathToFileURL(resolveCodemodeRuntimeAsset(localPath, join("kernels", "js", "worker-entry.js"), options));
|
|
29
|
-
}
|
|
20
|
+
export { type JavaScriptWorkerEntryUrlOptions, resolveJsWorkerEntryUrl } from "./worker-startup.ts";
|
|
30
21
|
|
|
31
22
|
export class JavaScriptKernel {
|
|
32
23
|
readonly #options: JavaScriptKernelOptions;
|
|
33
24
|
readonly #moduleLoader: LocalModuleLoader;
|
|
34
|
-
#
|
|
35
|
-
#mode: JavaScriptKernelMode = "worker";
|
|
25
|
+
readonly #slot: WorkerSlot;
|
|
36
26
|
#lifecycle: LifecycleState = "open";
|
|
37
|
-
#ready: Promise<void> | null = null;
|
|
38
|
-
#startupAbort: AbortController | null = null;
|
|
39
|
-
#generation = 0;
|
|
40
27
|
#activation: Promise<void> | null = null;
|
|
41
28
|
#recovery: Promise<void> | null = null;
|
|
42
29
|
#closePromise: Promise<void> | null = null;
|
|
@@ -48,10 +35,15 @@ export class JavaScriptKernel {
|
|
|
48
35
|
constructor(options: JavaScriptKernelOptions) {
|
|
49
36
|
this.#options = options;
|
|
50
37
|
this.#moduleLoader = new LocalModuleLoader(options);
|
|
38
|
+
this.#slot = new WorkerSlot(options, {
|
|
39
|
+
isOpen: () => this.#lifecycle === "open",
|
|
40
|
+
onMessage: (message) => this.#handleMessage(message),
|
|
41
|
+
onCrash: (error) => this.#handleCrash(error),
|
|
42
|
+
});
|
|
51
43
|
}
|
|
52
44
|
|
|
53
45
|
get mode(): JavaScriptKernelMode {
|
|
54
|
-
return this.#mode;
|
|
46
|
+
return this.#slot.mode;
|
|
55
47
|
}
|
|
56
48
|
|
|
57
49
|
async run(input: JavaScriptRunInput): Promise<ResultMessage> {
|
|
@@ -64,13 +56,16 @@ export class JavaScriptKernel {
|
|
|
64
56
|
async interrupt(reason = "interrupted"): Promise<KernelInterruptHandle> {
|
|
65
57
|
assertJavaScriptKernelOpen(this.#lifecycle, "interrupt");
|
|
66
58
|
const active = this.#runs.active;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
if (!active) {
|
|
60
|
+
const queued = this.#runs.takeInterruptTarget();
|
|
61
|
+
if (!queued) return { stateRetained: Promise.resolve(true) };
|
|
62
|
+
this.#runs.settle(queued, stoppedResult(queued.input.cellId, `JS cell interrupted: ${reason}`));
|
|
63
|
+
await this.#restartAfterStop();
|
|
64
|
+
return { stateRetained: Promise.resolve(false) };
|
|
65
|
+
}
|
|
66
|
+
this.#clearTimeout();
|
|
67
|
+
const stop = await this.#stopActive(active, reason, `JS cell interrupted: ${reason}`);
|
|
68
|
+
return { stateRetained: Promise.resolve(stop.retained), ...(stop.note === undefined ? {} : { note: stop.note }) };
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
async reset(): Promise<void> {
|
|
@@ -82,7 +77,7 @@ export class JavaScriptKernel {
|
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
deliverToolReply(message: Extract<HostToKernelMessage, { type: "tool-reply" }>): void {
|
|
85
|
-
if (this.#lifecycle === "open") this.#
|
|
80
|
+
if (this.#lifecycle === "open") this.#slot.postMessage(message);
|
|
86
81
|
}
|
|
87
82
|
|
|
88
83
|
async nextToolCall(): Promise<ToolCallMessage> {
|
|
@@ -93,7 +88,7 @@ export class JavaScriptKernel {
|
|
|
93
88
|
|
|
94
89
|
async close(): Promise<void> {
|
|
95
90
|
if (this.#closePromise) return await this.#closePromise;
|
|
96
|
-
this.#
|
|
91
|
+
this.#slot.postMessage({ type: "close" });
|
|
97
92
|
this.#lifecycle = "closing";
|
|
98
93
|
this.#runs.settleAll("JS kernel closed");
|
|
99
94
|
const recovery = this.#recovery;
|
|
@@ -129,92 +124,17 @@ export class JavaScriptKernel {
|
|
|
129
124
|
|
|
130
125
|
async #ensureReady(): Promise<void> {
|
|
131
126
|
assertJavaScriptKernelOpen(this.#lifecycle, "run");
|
|
132
|
-
|
|
133
|
-
const generation = ++this.#generation;
|
|
134
|
-
const controller = new AbortController();
|
|
135
|
-
this.#startupAbort = controller;
|
|
136
|
-
const ready = this.#startWorker(generation, controller.signal);
|
|
137
|
-
this.#ready = ready;
|
|
138
|
-
void ready.then(
|
|
139
|
-
() => {
|
|
140
|
-
if (this.#ready === ready) this.#startupAbort = null;
|
|
141
|
-
},
|
|
142
|
-
() => {
|
|
143
|
-
if (this.#ready === ready) {
|
|
144
|
-
this.#ready = null;
|
|
145
|
-
this.#startupAbort = null;
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
return await this.#ready;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async #startWorker(generation: number, signal: AbortSignal): Promise<void> {
|
|
154
|
-
let worker = this.#spawnWorker();
|
|
155
|
-
this.#publishWorker(worker, generation);
|
|
156
|
-
try {
|
|
157
|
-
await this.#initializeWorker(worker, signal);
|
|
158
|
-
return;
|
|
159
|
-
} catch (error) {
|
|
160
|
-
if (!this.#isCurrent(worker, generation) || error instanceof WorkerStartupCancelledError) {
|
|
161
|
-
await worker.terminate();
|
|
162
|
-
throw new WorkerStartupCancelledError();
|
|
163
|
-
}
|
|
164
|
-
if (worker.mode === "inline") throw error;
|
|
165
|
-
this.#worker = null;
|
|
166
|
-
await worker.terminate();
|
|
167
|
-
}
|
|
168
|
-
if (this.#lifecycle !== "open" || generation !== this.#generation) throw new WorkerStartupCancelledError();
|
|
169
|
-
worker = createInlineWorker(this.#options.cwd, this.#options.parallelPoolWidth);
|
|
170
|
-
this.#publishWorker(worker, generation);
|
|
171
|
-
await this.#initializeWorker(worker, signal);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
#spawnWorker(): WorkerLike {
|
|
175
|
-
try {
|
|
176
|
-
const url = this.#options.workerEntryUrl ?? resolveJsWorkerEntryUrl();
|
|
177
|
-
return spawnNodeWorker(url, this.#options.cwd, this.#options.parallelPoolWidth);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
if (!(error instanceof Error)) throw error;
|
|
180
|
-
return createInlineWorker(this.#options.cwd, this.#options.parallelPoolWidth);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
#publishWorker(worker: WorkerLike, generation: number): void {
|
|
185
|
-
if (this.#lifecycle !== "open" || generation !== this.#generation) throw new WorkerStartupCancelledError();
|
|
186
|
-
this.#worker = worker;
|
|
187
|
-
this.#mode = worker.mode;
|
|
188
|
-
worker.onMessage((message) => {
|
|
189
|
-
if (this.#isCurrent(worker, generation)) this.#handleMessage(message);
|
|
190
|
-
});
|
|
191
|
-
worker.onError((error) => {
|
|
192
|
-
if (this.#isCurrent(worker, generation)) this.#handleCrash(error);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async #initializeWorker(worker: WorkerLike, signal: AbortSignal): Promise<void> {
|
|
197
|
-
const ready = waitForReady(worker, signal);
|
|
198
|
-
worker.postMessage({
|
|
199
|
-
type: "init",
|
|
200
|
-
sessionId: this.#options.sessionId,
|
|
201
|
-
connection: localBridgeConnection(this.#options),
|
|
202
|
-
});
|
|
203
|
-
await ready;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
#isCurrent(worker: WorkerLike, generation: number): boolean {
|
|
207
|
-
return this.#lifecycle === "open" && this.#worker === worker && this.#generation === generation;
|
|
127
|
+
await this.#slot.ensureReady();
|
|
208
128
|
}
|
|
209
129
|
|
|
210
130
|
#startNext(): void {
|
|
211
|
-
if (this.#lifecycle !== "open" || this.#runs.active || !this.#
|
|
131
|
+
if (this.#lifecycle !== "open" || this.#runs.active || !this.#slot.present) return;
|
|
212
132
|
const next = this.#runs.startNext(performance.now());
|
|
213
133
|
if (!next) return;
|
|
214
134
|
if (next.input.timeoutMs) {
|
|
215
135
|
this.#timeout = setTimeout(() => void this.#timeoutActive(next), next.input.timeoutMs);
|
|
216
136
|
}
|
|
217
|
-
this.#
|
|
137
|
+
this.#slot.postMessage({
|
|
218
138
|
type: "run",
|
|
219
139
|
cellId: next.input.cellId,
|
|
220
140
|
code: this.#moduleLoader.prepareCell(next.input.code),
|
|
@@ -223,21 +143,46 @@ export class JavaScriptKernel {
|
|
|
223
143
|
}
|
|
224
144
|
|
|
225
145
|
async #timeoutActive(run: PendingJavaScriptRun): Promise<void> {
|
|
226
|
-
if (
|
|
146
|
+
if (this.#runs.active !== run || run.settled) return;
|
|
227
147
|
const durationMs = run.input.timeoutMs ?? 0;
|
|
228
|
-
this.#
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
error: { message: `JS cell timed out after ${durationMs}ms` },
|
|
148
|
+
await this.#stopActive(
|
|
149
|
+
run,
|
|
150
|
+
`timed out after ${durationMs}ms`,
|
|
151
|
+
`JS cell timed out after ${durationMs}ms`,
|
|
233
152
|
durationMs,
|
|
234
|
-
|
|
235
|
-
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Asks the worker to settle the active cell cooperatively (rejecting its bridge calls and killing its
|
|
158
|
+
* children); only a cell that stays unsettled past the grace costs the worker VM. Reports whether the
|
|
159
|
+
* worker state survived and, when a blocked worker had to be abandoned, the note that explains it.
|
|
160
|
+
*/
|
|
161
|
+
async #stopActive(
|
|
162
|
+
run: PendingJavaScriptRun,
|
|
163
|
+
reason: string,
|
|
164
|
+
message: string,
|
|
165
|
+
durationMs = 0,
|
|
166
|
+
): Promise<{ readonly retained: boolean; readonly note?: string }> {
|
|
167
|
+
run.interruptResult = { type: "result", cellId: run.input.cellId, ok: false, error: { message }, durationMs };
|
|
168
|
+
run.interruptAck ??= Promise.withResolvers<void>();
|
|
169
|
+
this.#slot.postMessage({ type: "interrupt", reason });
|
|
170
|
+
if ((await awaitCooperativeSettlement(run)) === "settled") return { retained: run.settledByWorker };
|
|
171
|
+
if (!this.#runs.releaseActive(run)) return { retained: run.settledByWorker };
|
|
172
|
+
const retirement = await this.#terminate();
|
|
173
|
+
this.#runs.settle(run, run.interruptResult ?? stoppedResult(run.input.cellId, message));
|
|
174
|
+
void this.#recover(() => Promise.resolve());
|
|
175
|
+
return retirement === "abandoned" ? { retained: false, note: abandonedWorkerNote() } : { retained: false };
|
|
236
176
|
}
|
|
237
177
|
|
|
238
178
|
async #restartAfterStop(): Promise<void> {
|
|
179
|
+
await this.#recover(() => this.#terminate());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** One recovery at a time: retire through `retire` (a no-op when the worker is already gone), then bring a fresh worker up. */
|
|
183
|
+
async #recover(retire: () => Promise<unknown>): Promise<void> {
|
|
239
184
|
if (this.#recovery) return await this.#recovery;
|
|
240
|
-
const recovery = this.#
|
|
185
|
+
const recovery = this.#performRecovery(retire);
|
|
241
186
|
this.#recovery = recovery;
|
|
242
187
|
try {
|
|
243
188
|
await recovery;
|
|
@@ -246,9 +191,9 @@ export class JavaScriptKernel {
|
|
|
246
191
|
}
|
|
247
192
|
}
|
|
248
193
|
|
|
249
|
-
async #
|
|
194
|
+
async #performRecovery(retire: () => Promise<unknown>): Promise<void> {
|
|
250
195
|
try {
|
|
251
|
-
await
|
|
196
|
+
await retire();
|
|
252
197
|
if (this.#lifecycle !== "open") return;
|
|
253
198
|
await this.#ensureReady();
|
|
254
199
|
if (this.#lifecycle === "open") this.#startNext();
|
|
@@ -259,6 +204,10 @@ export class JavaScriptKernel {
|
|
|
259
204
|
}
|
|
260
205
|
|
|
261
206
|
#handleMessage(message: KernelToHostMessage): void {
|
|
207
|
+
if (message.type === "status" && message.event.op === INTERRUPT_ACK_OP) {
|
|
208
|
+
this.#runs.active?.interruptAck?.resolve();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
262
211
|
this.#options.onMessage?.(message);
|
|
263
212
|
this.#runs.active?.input.onMessage?.(message);
|
|
264
213
|
if (message.type === "tool-call") {
|
|
@@ -272,13 +221,14 @@ export class JavaScriptKernel {
|
|
|
272
221
|
if (!active || active.input.cellId !== message.cellId) return;
|
|
273
222
|
this.#clearTimeout();
|
|
274
223
|
this.#runs.releaseActive(active);
|
|
275
|
-
|
|
224
|
+
active.settledByWorker = true;
|
|
225
|
+
this.#runs.settle(active, active.interruptResult ?? message);
|
|
276
226
|
this.#startNext();
|
|
277
227
|
}
|
|
278
228
|
|
|
279
229
|
#handleCrash(error: Error): void {
|
|
280
230
|
const active = this.#runs.active;
|
|
281
|
-
if (!active && this.#
|
|
231
|
+
if (!active && this.#slot.startingUp) return;
|
|
282
232
|
this.#clearTimeout();
|
|
283
233
|
if (active) {
|
|
284
234
|
this.#runs.releaseActive(active);
|
|
@@ -298,14 +248,8 @@ export class JavaScriptKernel {
|
|
|
298
248
|
this.#timeout = null;
|
|
299
249
|
}
|
|
300
250
|
|
|
301
|
-
async #terminate(): Promise<
|
|
251
|
+
async #terminate(): Promise<WorkerRetirement> {
|
|
302
252
|
this.#clearTimeout();
|
|
303
|
-
this.#
|
|
304
|
-
this.#startupAbort?.abort();
|
|
305
|
-
this.#startupAbort = null;
|
|
306
|
-
this.#ready = null;
|
|
307
|
-
const worker = this.#worker;
|
|
308
|
-
this.#worker = null;
|
|
309
|
-
if (worker) await worker.terminate();
|
|
253
|
+
return await this.#slot.retire();
|
|
310
254
|
}
|
|
311
255
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
4
|
-
import { type CodemodeRuntimeAssetEnvironment,
|
|
4
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
5
5
|
import type { JavaScriptKernelMode } from "./kernel-contract.ts";
|
|
6
6
|
import { spawnNodeWorker } from "./worker-host.ts";
|
|
7
7
|
|
|
@@ -12,7 +12,7 @@ export interface JavaScriptInlineWorkerEntryUrlOptions extends CodemodeRuntimeAs
|
|
|
12
12
|
export function resolveInlineWorkerEntryUrl(options: JavaScriptInlineWorkerEntryUrlOptions = {}): URL {
|
|
13
13
|
const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "inline-worker-entry.js");
|
|
14
14
|
return pathToFileURL(
|
|
15
|
-
|
|
15
|
+
requireCodemodeRuntimeAsset(localPath, join("kernels", "js", "inline-worker-entry.js"), options),
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
|