@code-yeongyu/senpi 2026.8.21 → 2026.8.22-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 +69 -0
- package/dist/bun-runtime.d.ts +58 -0
- package/dist/bun-runtime.d.ts.map +1 -0
- package/dist/bun-runtime.js +163 -0
- package/dist/bun-runtime.js.map +1 -0
- package/dist/cli.js +45 -2
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session.d.ts +7 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +81 -26
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-settled-delivery.d.ts +13 -1
- package/dist/core/agent-settled-delivery.d.ts.map +1 -1
- package/dist/core/agent-settled-delivery.js +33 -2
- package/dist/core/agent-settled-delivery.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +5 -4
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/extensions/builtin/goal/cache-warm-renderer.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +2 -3
- package/dist/core/extensions/builtin/goal/cache-warm-renderer.js.map +1 -1
- package/dist/core/extensions/builtin/goal/cache-warm.d.ts +6 -0
- package/dist/core/extensions/builtin/goal/cache-warm.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/cache-warm.js +43 -0
- package/dist/core/extensions/builtin/goal/cache-warm.js.map +1 -1
- package/dist/core/extensions/builtin/permission-system/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/permission-system/index.js +7 -11
- package/dist/core/extensions/builtin/permission-system/index.js.map +1 -1
- package/dist/core/model-resolver.js +2 -2
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/session-work-barrier.d.ts.map +1 -1
- package/dist/core/session-work-barrier.js +38 -1
- package/dist/core/session-work-barrier.js.map +1 -1
- package/dist/modes/interactive/components/status-indicator.d.ts +3 -0
- package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-indicator.js +7 -1
- package/dist/modes/interactive/components/status-indicator.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +118 -38
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/senpi +45 -2
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +58 -0
- package/node_modules/@code-yeongyu/senpi-codemode/README.md +5 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/extension/runtime-factory.ts +4 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/extension/runtime-info.ts +41 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +8 -3
- package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +4 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/interpreters/detect.ts +17 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/interpreters/resolve-command.ts +68 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +15 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/timeouts/idle-timeout.ts +33 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-runtime.ts +5 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool-options.ts +3 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +60 -3
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/runtime-label.ts +49 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +13 -0
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.d.ts +21 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.js +36 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.d.ts +10 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +166 -26
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/google.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
- package/dist/modes/interactive/split-trailing-assistant-text.d.ts +0 -7
- package/dist/modes/interactive/split-trailing-assistant-text.d.ts.map +0 -1
- package/dist/modes/interactive/split-trailing-assistant-text.js +0 -19
- package/dist/modes/interactive/split-trailing-assistant-text.js.map +0 -1
package/dist/senpi
CHANGED
|
@@ -1,13 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./valid-cwd.js";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { processBunRuntimeOptions, resolveBunReexec } from "./bun-runtime.js";
|
|
7
8
|
import { enableStartupCompileCache } from "./compile-cache.js";
|
|
8
9
|
import { APP_NAME, DISPLAY_VERSION, getPackageDir } from "./config.js";
|
|
9
10
|
import { hasInheritedInspectorOption, releaseInheritedInspectorForChild } from "./inspector-policy.js";
|
|
10
11
|
import { handleBootstrapSelfUpdate } from "./self-update-bootstrap.js";
|
|
12
|
+
/**
|
|
13
|
+
* Hand a Bun-installed CLI to Bun before anything else runs.
|
|
14
|
+
*
|
|
15
|
+
* `bun install -g` links this script into `~/.bun/bin`, but the shebang still starts it on Node,
|
|
16
|
+
* so a user who chose Bun silently gets the Node runtime. Re-exec through the Bun binary when the
|
|
17
|
+
* script really lives in Bun's global tree (or `SENPI_RUNTIME=bun` asks for it). This runs before
|
|
18
|
+
* `enableStartupCompileCache()` on purpose: a re-exec must not pay for Node's compile-cache setup.
|
|
19
|
+
* Node `execArgv` is deliberately dropped — those flags belong to the Node process, not to Bun.
|
|
20
|
+
*/
|
|
21
|
+
function reexecUnderBunIfNeeded() {
|
|
22
|
+
const options = processBunRuntimeOptions(existsSync, realpathSync);
|
|
23
|
+
let scriptRealPath = process.argv[1] ?? fileURLToPath(import.meta.url);
|
|
24
|
+
try {
|
|
25
|
+
// `~/.bun/bin/<name>` is a symlink into the global tree, so the link target is what has
|
|
26
|
+
// to be classified and re-executed. A path that cannot be resolved is simply used as-is;
|
|
27
|
+
// runtime selection must never be the reason startup fails.
|
|
28
|
+
scriptRealPath = realpathSync(scriptRealPath);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
const decision = resolveBunReexec({
|
|
32
|
+
scriptRealPath,
|
|
33
|
+
versions: process.versions,
|
|
34
|
+
hasInheritedInspectorOption: hasInheritedInspectorOption(),
|
|
35
|
+
options,
|
|
36
|
+
});
|
|
37
|
+
if (decision.action === "stay") {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const result = spawnSync(decision.bunPath, [scriptRealPath, ...process.argv.slice(2)], {
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
windowsHide: true,
|
|
43
|
+
});
|
|
44
|
+
if (result.signal) {
|
|
45
|
+
process.kill(process.pid, result.signal);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
process.exitCode = result.status ?? 1;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (reexecUnderBunIfNeeded()) {
|
|
52
|
+
process.exit();
|
|
53
|
+
}
|
|
11
54
|
// Must run before cli-main is loaded, by either path: it caches the engine graph this process
|
|
12
55
|
// imports on the fast path below, and it publishes NODE_COMPILE_CACHE so a spawned cli-main child
|
|
13
56
|
// inherits this process's cache directory instead of resolving and re-filling its own.
|
|
@@ -12,6 +12,64 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.8.22-2] - 2026-08-22
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Eval headers now display the kernel runtime identity, e.g. `eval py (3.14.7, ~/.venv/bin/python3)` and `eval js (node 26.7.0, /opt/…/bin/node)`; the same `runtime` info rides `EvalToolDetails` and its `cells` so RPC consumers receive it, interpreter detection resolves absolute executable paths from PATH, and the eval prompt host line names the JS runtime (`node`/`bun` with version).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Running eval cell headers now tick their elapsed time in real time (`eval py running · 13s`) instead of freezing between kernel update events; the renderer derives elapsed time from a render-time clock while a cell is pending/running/detached and repaints once per second, while settled cells keep their exact final duration. `EvalCellResult` gains an additive `startedAt` so RPC consumers can compute the same live value.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- A host tool call from inside an eval cell no longer suspends the cell's timeout indefinitely. The idle watchdog previously cleared its timer for the entire duration of a bridge call, so a call that never returned (e.g. an awaited `dag-wait`) left the cell pending — and the agent loop parked, queueing user messages invisibly — until the 1800s hard limit. The pause is now bounded by a max pause grace (default 600s, floored at the cell's own `timeout`): a long bridge call such as a 5-minute build still runs to completion, but a stuck one now trips the cell's `on_timeout` handling and releases the loop.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
## [2026.8.22] - 2026-08-22
|
|
34
|
+
|
|
35
|
+
### Breaking Changes
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Ruby and Julia eval cells now wait for the subprocess `ready` signal before execution timeouts begin, so interpreter startup under load cannot time out a state-setting cell and silently restart the kernel before the next cell runs.
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
|
|
47
|
+
## [2026.8.21-3] - 2026-08-21
|
|
48
|
+
|
|
49
|
+
### Breaking Changes
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
## [2026.8.21-2] - 2026-08-21
|
|
60
|
+
|
|
61
|
+
### Breaking Changes
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- `js` eval cells now accept `local://` paths in `read()` and `write()` like every other kernel. The session manager computed the session local root only after its `language === "js"` early return, so the JavaScript kernel was constructed without `localRoots` or `artifactsDir` and every `local://` helper call failed with `Protocol paths are not supported by write()`, even though the JavaScript prelude documents `local://` as the session local root. `py`/`rb`/`jl` behavior is unchanged.
|
|
70
|
+
|
|
71
|
+
### Removed
|
|
72
|
+
|
|
15
73
|
## [2026.8.21] - 2026-08-21
|
|
16
74
|
|
|
17
75
|
### Breaking Changes
|
|
@@ -30,6 +30,11 @@ task-tool names are known.
|
|
|
30
30
|
- TUI and HTML-export rendering for syntax-highlighted cells, status rows,
|
|
31
31
|
task progress, structured display values, truncation warnings, and image
|
|
32
32
|
fallbacks.
|
|
33
|
+
- Runtime identity badges in eval headers — `eval py (3.14.7, ~/.venv/bin/python3)`,
|
|
34
|
+
`eval js (node 26.7.0, /opt/…/bin/node)` — with the same `runtime` info on
|
|
35
|
+
`EvalToolDetails` and its `cells` for RPC consumers; interpreter detection
|
|
36
|
+
resolves absolute executable paths, and the eval prompt host line names the
|
|
37
|
+
JS runtime (`node`/`bun`).
|
|
33
38
|
- JavaScript import rewriting for supported local modules and package imports
|
|
34
39
|
in the persistent Node.js worker.
|
|
35
40
|
- GPT models receive a terse `eval` prompt dialect that prioritizes composing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.22-2",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
6
6
|
"type": "module",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@babel/parser": "8.0.4",
|
|
34
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.
|
|
34
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.22-2",
|
|
35
35
|
"typebox": "1.3.16"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@code-yeongyu/senpi": "2026.8.
|
|
38
|
+
"@code-yeongyu/senpi": "2026.8.22-2"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@code-yeongyu/senpi": "2026.8.
|
|
41
|
+
"@code-yeongyu/senpi": "2026.8.22-2"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
44
|
"senpi",
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
type InterpreterAvailability,
|
|
15
15
|
} from "../interpreters/detect.ts";
|
|
16
16
|
import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
|
|
17
|
-
import type { EnabledEvalLanguages, EvalLanguage } from "../tool/types.ts";
|
|
17
|
+
import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
|
|
18
|
+
import { jsRuntimeInfo, runtimesFromAvailability } from "./runtime-info.ts";
|
|
18
19
|
import {
|
|
19
20
|
type CodemodeSessionManager,
|
|
20
21
|
type CreateCodemodeSessionManagerOptions,
|
|
@@ -39,6 +40,7 @@ export type SessionRuntime = {
|
|
|
39
40
|
readonly parallelPoolWidth: number;
|
|
40
41
|
readonly manager: CodemodeSessionManager;
|
|
41
42
|
readonly enabledLanguages: EnabledEvalLanguages;
|
|
43
|
+
readonly runtimes: EvalRuntimes;
|
|
42
44
|
readonly settings: ResolvedCodemodeSettings;
|
|
43
45
|
readonly artifactsDir: string;
|
|
44
46
|
readonly executeTool: AgentExecuteTool;
|
|
@@ -82,6 +84,7 @@ export async function createRuntime(
|
|
|
82
84
|
parallelPoolWidth,
|
|
83
85
|
manager,
|
|
84
86
|
enabledLanguages,
|
|
87
|
+
runtimes: runtimesFromAvailability(availability, jsRuntimeInfo()),
|
|
85
88
|
settings,
|
|
86
89
|
artifactsDir: artifacts.dir,
|
|
87
90
|
executeTool,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { InterpreterAvailability } from "../interpreters/detect.ts";
|
|
2
|
+
import type { EvalLanguage, EvalRuntimeInfo, EvalRuntimes } from "../tool/types.ts";
|
|
3
|
+
|
|
4
|
+
export interface JsRuntimeVersions {
|
|
5
|
+
readonly node: string;
|
|
6
|
+
readonly bun?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Identity of the in-process JS kernel host: bun when its marker exists, node otherwise. */
|
|
10
|
+
export function jsRuntimeInfo(
|
|
11
|
+
versions: JsRuntimeVersions = process.versions,
|
|
12
|
+
execPath: string = process.execPath,
|
|
13
|
+
): EvalRuntimeInfo {
|
|
14
|
+
const bun = versions.bun;
|
|
15
|
+
if (bun !== undefined && bun.length > 0) return { name: "bun", version: bun, path: execPath };
|
|
16
|
+
return { name: "node", version: versions.node, path: execPath };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Short host-line segment, e.g. "node 26.7.0" or "bun 1.4.0". */
|
|
20
|
+
export function jsRuntimeLabel(versions: JsRuntimeVersions = process.versions): string {
|
|
21
|
+
const info = jsRuntimeInfo(versions, "");
|
|
22
|
+
return `${info.name} ${info.version}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const subprocessRuntimeNames = { py: "python", rb: "ruby", jl: "julia" } as const;
|
|
26
|
+
const subprocessLanguages = ["py", "rb", "jl"] as const;
|
|
27
|
+
|
|
28
|
+
/** Maps detected interpreters to display runtimes, preferring resolved absolute paths. */
|
|
29
|
+
export function runtimesFromAvailability(availability: InterpreterAvailability, js: EvalRuntimeInfo): EvalRuntimes {
|
|
30
|
+
const runtimes: Partial<Record<EvalLanguage, EvalRuntimeInfo>> = { js };
|
|
31
|
+
for (const language of subprocessLanguages) {
|
|
32
|
+
const detected = availability[language].detected;
|
|
33
|
+
if (!detected.ok) continue;
|
|
34
|
+
runtimes[language] = {
|
|
35
|
+
name: subprocessRuntimeNames[language],
|
|
36
|
+
version: detected.version,
|
|
37
|
+
path: detected.resolvedPath ?? detected.path,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return runtimes;
|
|
41
|
+
}
|
|
@@ -196,19 +196,24 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
196
196
|
if (!bridge) throw new Error("codemode bridge server is not running");
|
|
197
197
|
const configuredPoolWidth = this.#options.settings.parallelPoolWidth;
|
|
198
198
|
const parallelPoolWidth = Number.isFinite(configuredPoolWidth) ? Math.max(1, Math.trunc(configuredPoolWidth)) : 1;
|
|
199
|
+
// localRoots must be computed BEFORE the js branch: the JS kernel resolves local://
|
|
200
|
+
// from its worker init connection exactly like the subprocess kernels resolve it from
|
|
201
|
+
// theirs. Computing it after the early return left js cells with no local root at all.
|
|
202
|
+
const localRoots =
|
|
203
|
+
this.#options.localRoots ??
|
|
204
|
+
(this.#options.artifactsDir ? { local: join(this.#options.artifactsDir, "local") } : undefined);
|
|
199
205
|
if (language === "js") {
|
|
200
206
|
return new JavaScriptKernel({
|
|
201
207
|
sessionId: this.#options.sessionId,
|
|
202
208
|
cwd: this.#options.cwd,
|
|
203
209
|
parallelPoolWidth,
|
|
204
210
|
onMessage,
|
|
211
|
+
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
212
|
+
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
205
213
|
});
|
|
206
214
|
}
|
|
207
215
|
const detected = this.#options.availability[language].detected;
|
|
208
216
|
if (!detected.ok) throw new Error(`No ${language} interpreter is available`);
|
|
209
|
-
const localRoots =
|
|
210
|
-
this.#options.localRoots ??
|
|
211
|
-
(this.#options.artifactsDir ? { local: join(this.#options.artifactsDir, "local") } : undefined);
|
|
212
217
|
const connection = {
|
|
213
218
|
port: bridge.port,
|
|
214
219
|
token: bridge.token,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
enabledLanguagesFrom,
|
|
14
14
|
type SessionRuntime,
|
|
15
15
|
} from "./extension/runtime-factory.ts";
|
|
16
|
+
import { jsRuntimeInfo, jsRuntimeLabel } from "./extension/runtime-info.ts";
|
|
16
17
|
import type { CodemodeSessionManager, CreateCodemodeSessionManagerOptions } from "./extension/session-manager.ts";
|
|
17
18
|
import { SessionManagerProxy } from "./extension/session-manager-proxy.ts";
|
|
18
19
|
import { WAKE_SOURCE_STATE_EVENT, type WakeSourceState } from "./extension/wake-source-state.ts";
|
|
@@ -119,6 +120,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
119
120
|
spawns: runtime.spawns,
|
|
120
121
|
spawnDefaultAgent: runtime.settings.taskTools.task,
|
|
121
122
|
hostLine: hostLine(),
|
|
123
|
+
runtimes: runtime.runtimes,
|
|
122
124
|
...(modelId === undefined ? {} : { modelId }),
|
|
123
125
|
}),
|
|
124
126
|
);
|
|
@@ -152,6 +154,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
152
154
|
executionTracker: manager,
|
|
153
155
|
renderers,
|
|
154
156
|
hostLine: hostLine(),
|
|
157
|
+
runtimes: { js: jsRuntimeInfo() },
|
|
155
158
|
}),
|
|
156
159
|
);
|
|
157
160
|
pi.registerRemovedToolHint(
|
|
@@ -206,7 +209,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
206
209
|
|
|
207
210
|
function hostLine(): string {
|
|
208
211
|
const cpu = os.cpus()[0]?.model?.trim();
|
|
209
|
-
return [`${os.platform()} ${os.arch()}`, cpu, `${os.availableParallelism()} cores
|
|
212
|
+
return [`${os.platform()} ${os.arch()}`, cpu, `${os.availableParallelism()} cores`, jsRuntimeLabel()]
|
|
210
213
|
.filter((part): part is string => !!part)
|
|
211
214
|
.join(" \u00b7 ");
|
|
212
215
|
}
|
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { platform as currentPlatform } from "node:os";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import type { CodemodeSettings } from "../config/settings.ts";
|
|
5
|
+
import { resolveCommandPath as defaultResolveCommandPath, type ResolveCommandPath } from "./resolve-command.ts";
|
|
5
6
|
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
const probeTimeoutMs = 3_000;
|
|
@@ -10,8 +11,11 @@ export type CodemodeLanguage = "py" | "js" | "rb" | "jl";
|
|
|
10
11
|
|
|
11
12
|
export interface InterpreterDetected {
|
|
12
13
|
readonly ok: true;
|
|
14
|
+
/** The probe command line that answered, e.g. "python3" or "py -3". */
|
|
13
15
|
readonly path: string;
|
|
14
16
|
readonly version: string;
|
|
17
|
+
/** Absolute executable path resolved from PATH, when resolution succeeded. */
|
|
18
|
+
readonly resolvedPath?: string;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export interface InterpreterUnavailable {
|
|
@@ -34,6 +38,7 @@ export interface CreateInterpreterDetectorOptions {
|
|
|
34
38
|
readonly platform?: NodeJS.Platform;
|
|
35
39
|
readonly execFile?: ExecFileProbe;
|
|
36
40
|
readonly nodeVersion?: string;
|
|
41
|
+
readonly resolveCommandPath?: ResolveCommandPath;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export interface InterpreterDetector {
|
|
@@ -55,6 +60,7 @@ export function createInterpreterDetector(options: CreateInterpreterDetectorOpti
|
|
|
55
60
|
const probe = options.execFile ?? defaultExecFileProbe;
|
|
56
61
|
const hostPlatform = options.platform ?? currentPlatform();
|
|
57
62
|
const nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
63
|
+
const resolveCommand = options.resolveCommandPath ?? defaultResolveCommandPath;
|
|
58
64
|
const cache = new Map<CodemodeLanguage, Promise<InterpreterDetection>>();
|
|
59
65
|
|
|
60
66
|
return {
|
|
@@ -64,7 +70,7 @@ export function createInterpreterDetector(options: CreateInterpreterDetectorOpti
|
|
|
64
70
|
return cached;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
const pending = detectUncached(language, hostPlatform, probe, nodeVersion);
|
|
73
|
+
const pending = detectUncached(language, hostPlatform, probe, nodeVersion, resolveCommand);
|
|
68
74
|
cache.set(language, pending);
|
|
69
75
|
return pending;
|
|
70
76
|
},
|
|
@@ -99,13 +105,14 @@ async function detectUncached(
|
|
|
99
105
|
hostPlatform: NodeJS.Platform,
|
|
100
106
|
probe: ExecFileProbe,
|
|
101
107
|
nodeVersion: string,
|
|
108
|
+
resolveCommand: ResolveCommandPath,
|
|
102
109
|
): Promise<InterpreterDetection> {
|
|
103
110
|
if (language === "js") {
|
|
104
111
|
return { ok: true, path: "node", version: nodeVersion };
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
for (const candidate of candidatesFor(language, hostPlatform)) {
|
|
108
|
-
const result = await probeCandidate(candidate, probe);
|
|
115
|
+
const result = await probeCandidate(candidate, probe, resolveCommand);
|
|
109
116
|
if (result.ok) {
|
|
110
117
|
return result;
|
|
111
118
|
}
|
|
@@ -114,12 +121,18 @@ async function detectUncached(
|
|
|
114
121
|
return unavailable;
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
async function probeCandidate(
|
|
124
|
+
async function probeCandidate(
|
|
125
|
+
candidate: string,
|
|
126
|
+
probe: ExecFileProbe,
|
|
127
|
+
resolveCommand: ResolveCommandPath,
|
|
128
|
+
): Promise<InterpreterDetection> {
|
|
118
129
|
const invocation = candidateInvocation(candidate);
|
|
119
130
|
try {
|
|
120
131
|
const result = await probe(invocation.command, [...invocation.args, "--version"], { timeoutMs: probeTimeoutMs });
|
|
121
132
|
const version = parseVersion(`${result.stdout}\n${result.stderr}`);
|
|
122
|
-
|
|
133
|
+
if (version === null) return unavailable;
|
|
134
|
+
const resolvedPath = resolveCommand(invocation.command);
|
|
135
|
+
return { ok: true, path: candidate, version, ...(resolvedPath === undefined ? {} : { resolvedPath }) };
|
|
123
136
|
} catch {
|
|
124
137
|
return unavailable;
|
|
125
138
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
2
|
+
import { delimiter, isAbsolute, resolve as resolvePath } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface ResolveCommandPathOptions {
|
|
5
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
6
|
+
readonly platform?: NodeJS.Platform;
|
|
7
|
+
readonly cwd?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ResolveCommandPath = (command: string, options?: ResolveCommandPathOptions) => string | undefined;
|
|
11
|
+
|
|
12
|
+
const defaultWindowsPathExt = ".COM;.EXE;.BAT;.CMD";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a bare command name to the absolute executable path a spawn would
|
|
16
|
+
* use, by scanning PATH without spawning a process. Returns undefined when the
|
|
17
|
+
* command cannot be resolved; callers treat that as "no display path known".
|
|
18
|
+
*/
|
|
19
|
+
export function resolveCommandPath(command: string, options: ResolveCommandPathOptions = {}): string | undefined {
|
|
20
|
+
if (command.length === 0) return undefined;
|
|
21
|
+
const env = options.env ?? process.env;
|
|
22
|
+
const platform = options.platform ?? process.platform;
|
|
23
|
+
const isWindows = platform === "win32";
|
|
24
|
+
if (command.includes("/") || (isWindows && command.includes("\\"))) {
|
|
25
|
+
const absolute = isAbsolute(command) ? command : resolvePath(options.cwd ?? process.cwd(), command);
|
|
26
|
+
return firstExecutable(candidatesFor(absolute, isWindows, env), isWindows);
|
|
27
|
+
}
|
|
28
|
+
const pathValue = env.PATH ?? env.Path ?? "";
|
|
29
|
+
if (pathValue.length === 0) return undefined;
|
|
30
|
+
for (const directory of pathValue.split(delimiter)) {
|
|
31
|
+
if (directory.length === 0) continue;
|
|
32
|
+
const base = `${directory}${directory.endsWith("/") || directory.endsWith("\\") ? "" : pathSeparatorFor(directory, isWindows)}${command}`;
|
|
33
|
+
const found = firstExecutable(candidatesFor(base, isWindows, env), isWindows);
|
|
34
|
+
if (found !== undefined) return found;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pathSeparatorFor(directory: string, isWindows: boolean): string {
|
|
40
|
+
if (isWindows && directory.includes("\\") && !directory.includes("/")) return "\\";
|
|
41
|
+
return "/";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function candidatesFor(base: string, isWindows: boolean, env: NodeJS.ProcessEnv): readonly string[] {
|
|
45
|
+
if (!isWindows) return [base];
|
|
46
|
+
const extensions = (env.PATHEXT ?? defaultWindowsPathExt)
|
|
47
|
+
.split(";")
|
|
48
|
+
.map((extension) => extension.trim())
|
|
49
|
+
.filter((extension) => extension.startsWith("."));
|
|
50
|
+
const candidates = [base];
|
|
51
|
+
for (const extension of extensions) {
|
|
52
|
+
candidates.push(`${base}${extension.toLowerCase()}`, `${base}${extension}`);
|
|
53
|
+
}
|
|
54
|
+
return candidates;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function firstExecutable(candidates: readonly string[], isWindows: boolean): string | undefined {
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
try {
|
|
60
|
+
if (!statSync(candidate).isFile()) continue;
|
|
61
|
+
if (!isWindows) accessSync(candidate, constants.X_OK);
|
|
62
|
+
return candidate;
|
|
63
|
+
} catch {
|
|
64
|
+
// Missing or non-executable candidate: keep scanning the remaining ones.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
@@ -26,6 +26,7 @@ export class SubprocessKernel {
|
|
|
26
26
|
private readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
27
27
|
private readonly runs = new SubprocessRunQueue();
|
|
28
28
|
private process: SubprocessProcess | null = null;
|
|
29
|
+
private processReady = false;
|
|
29
30
|
private retirementPromise: Promise<void> | null = null;
|
|
30
31
|
private retirementProcess: SubprocessProcess | null = null;
|
|
31
32
|
private retirementFailure: Error | null = null;
|
|
@@ -109,7 +110,7 @@ export class SubprocessKernel {
|
|
|
109
110
|
|
|
110
111
|
private pumpRuns(): void {
|
|
111
112
|
const process = this.process;
|
|
112
|
-
if (this.closed || this.runs.active || !process || process.isRetiring) return;
|
|
113
|
+
if (this.closed || this.runs.active || !process || process.isRetiring || !this.processReady) return;
|
|
113
114
|
const run = this.runs.startNext(performance.now());
|
|
114
115
|
if (!run) return;
|
|
115
116
|
const timeoutMs = run.input.timeoutMs;
|
|
@@ -142,6 +143,7 @@ export class SubprocessKernel {
|
|
|
142
143
|
},
|
|
143
144
|
});
|
|
144
145
|
this.process = process;
|
|
146
|
+
this.processReady = false;
|
|
145
147
|
try {
|
|
146
148
|
process.send(
|
|
147
149
|
encodeBridgeFrame({ type: "init", sessionId: this.options.sessionId, connection: this.options.connection }),
|
|
@@ -165,6 +167,17 @@ export class SubprocessKernel {
|
|
|
165
167
|
|
|
166
168
|
private handleMessage(process: SubprocessProcess, message: KernelToHostMessage): void {
|
|
167
169
|
if (!this.accepts(process)) return;
|
|
170
|
+
if (message.type === "ready") {
|
|
171
|
+
this.processReady = true;
|
|
172
|
+
this.runs.handleMessage(message, this.onMessage);
|
|
173
|
+
this.pumpRuns();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (message.type === "init-failed") {
|
|
177
|
+
this.runs.handleMessage(message, this.onMessage);
|
|
178
|
+
this.failClosed(new KernelStartupError(message.error.message));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
168
181
|
if (this.runs.handleMessage(message, this.onMessage)) this.pumpRuns();
|
|
169
182
|
}
|
|
170
183
|
|
|
@@ -176,6 +189,7 @@ export class SubprocessKernel {
|
|
|
176
189
|
return;
|
|
177
190
|
}
|
|
178
191
|
this.process = null;
|
|
192
|
+
this.processReady = false;
|
|
179
193
|
this.failClosed(new KernelExitedError(signal ?? code ?? "unknown"));
|
|
180
194
|
}
|
|
181
195
|
|
|
@@ -3,9 +3,18 @@ export interface IdleTimeoutEvent {
|
|
|
3
3
|
readonly error: Error;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Upper bound on how long a single host bridge call may suspend a cell's idle watchdog.
|
|
8
|
+
* Generous enough for a long build or a slow model call, short enough that a bridge call which
|
|
9
|
+
* never returns cannot park the cell — and with it the agent loop — until the 1800s hard limit.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_MAX_PAUSE_GRACE_MS = 600_000;
|
|
12
|
+
|
|
6
13
|
export interface IdleTimeoutOptions {
|
|
7
14
|
readonly cellId: string;
|
|
8
15
|
readonly timeoutMs: number;
|
|
16
|
+
/** Defaults to {@link DEFAULT_MAX_PAUSE_GRACE_MS}; floored at `timeoutMs` so a pause never shortens the budget. */
|
|
17
|
+
readonly maxPauseGraceMs?: number;
|
|
9
18
|
readonly onTimeout: (event: IdleTimeoutEvent) => void;
|
|
10
19
|
}
|
|
11
20
|
|
|
@@ -20,7 +29,9 @@ export class IdleTimeout implements TimeoutPauseHandle {
|
|
|
20
29
|
readonly #controller = new AbortController();
|
|
21
30
|
readonly signal = this.#controller.signal;
|
|
22
31
|
readonly timeoutMs: number;
|
|
32
|
+
readonly maxPauseGraceMs: number;
|
|
23
33
|
#deadlineMs: number;
|
|
34
|
+
#pausedDeadlineMs: number | undefined;
|
|
24
35
|
#timer: ReturnType<typeof setTimeout> | undefined;
|
|
25
36
|
#pauseDepth = 0;
|
|
26
37
|
#settled = false;
|
|
@@ -28,22 +39,32 @@ export class IdleTimeout implements TimeoutPauseHandle {
|
|
|
28
39
|
constructor(options: IdleTimeoutOptions) {
|
|
29
40
|
this.#cellId = options.cellId;
|
|
30
41
|
this.timeoutMs = Math.max(1, Math.floor(options.timeoutMs));
|
|
42
|
+
this.maxPauseGraceMs = Math.max(
|
|
43
|
+
this.timeoutMs,
|
|
44
|
+
Math.floor(options.maxPauseGraceMs ?? DEFAULT_MAX_PAUSE_GRACE_MS),
|
|
45
|
+
);
|
|
31
46
|
this.#deadlineMs = Date.now() + this.timeoutMs;
|
|
32
47
|
this.#onTimeout = options.onTimeout;
|
|
33
48
|
this.#arm(this.timeoutMs);
|
|
34
49
|
}
|
|
35
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Suspends the idle deadline for the duration of a host bridge call, but only up to the pause grace:
|
|
53
|
+
* the cell still expires if the call never returns. Nested pauses share the outermost pause's deadline.
|
|
54
|
+
*/
|
|
36
55
|
pause(): void {
|
|
37
56
|
if (this.#settled) return;
|
|
38
57
|
this.#pauseDepth++;
|
|
39
58
|
if (this.#pauseDepth !== 1) return;
|
|
40
|
-
this.#
|
|
59
|
+
this.#pausedDeadlineMs = Date.now() + this.maxPauseGraceMs;
|
|
60
|
+
this.#arm(this.maxPauseGraceMs);
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
resume(): void {
|
|
44
64
|
if (this.#settled || this.#pauseDepth === 0) return;
|
|
45
65
|
this.#pauseDepth--;
|
|
46
66
|
if (this.#pauseDepth > 0) return;
|
|
67
|
+
this.#pausedDeadlineMs = undefined;
|
|
47
68
|
this.#deadlineMs = Date.now() + this.timeoutMs;
|
|
48
69
|
this.#arm(this.timeoutMs);
|
|
49
70
|
}
|
|
@@ -51,6 +72,7 @@ export class IdleTimeout implements TimeoutPauseHandle {
|
|
|
51
72
|
dispose(): void {
|
|
52
73
|
if (this.#settled) return;
|
|
53
74
|
this.#settled = true;
|
|
75
|
+
this.#pausedDeadlineMs = undefined;
|
|
54
76
|
this.#clearTimer();
|
|
55
77
|
}
|
|
56
78
|
|
|
@@ -68,15 +90,22 @@ export class IdleTimeout implements TimeoutPauseHandle {
|
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
#expire(): void {
|
|
71
|
-
if (this.#settled
|
|
72
|
-
const
|
|
93
|
+
if (this.#settled) return;
|
|
94
|
+
const pausedDeadlineMs = this.#pausedDeadlineMs;
|
|
95
|
+
if (this.#pauseDepth > 0 && pausedDeadlineMs === undefined) return;
|
|
96
|
+
const deadlineMs = pausedDeadlineMs ?? this.#deadlineMs;
|
|
97
|
+
const remainingMs = deadlineMs - Date.now();
|
|
73
98
|
if (remainingMs > 0) {
|
|
74
99
|
this.#arm(remainingMs);
|
|
75
100
|
return;
|
|
76
101
|
}
|
|
77
102
|
this.#settled = true;
|
|
103
|
+
this.#pausedDeadlineMs = undefined;
|
|
78
104
|
this.#timer = undefined;
|
|
79
|
-
const error =
|
|
105
|
+
const error =
|
|
106
|
+
pausedDeadlineMs === undefined
|
|
107
|
+
? new Error(`Cell timed out after ${this.timeoutMs}ms`)
|
|
108
|
+
: new Error(`Cell timed out after ${this.maxPauseGraceMs}ms waiting on a host tool call`);
|
|
80
109
|
error.name = "TimeoutError";
|
|
81
110
|
this.#controller.abort(error);
|
|
82
111
|
this.#onTimeout({ cellId: this.#cellId, error });
|
|
@@ -2,7 +2,7 @@ import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from
|
|
|
2
2
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
3
|
import type { EvalToolCallMetric } from "./call-capture.ts";
|
|
4
4
|
import { type EvalImageResizer, EvalOutputCollector, type EvalOutputResult } from "./image.ts";
|
|
5
|
-
import type { EvalStatusEvent, EvalToolDetails, EvalToolInput } from "./types.ts";
|
|
5
|
+
import type { EvalRuntimeInfo, EvalStatusEvent, EvalToolDetails, EvalToolInput } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
type KernelResult = Extract<KernelToHostMessage, { type: "result" }>;
|
|
8
8
|
type DisplayMessage = Extract<KernelToHostMessage, { type: "display" }>;
|
|
@@ -10,6 +10,7 @@ type ToolCall = EvalToolDetails["toolCalls"] extends readonly (infer Item)[] ? I
|
|
|
10
10
|
|
|
11
11
|
export interface CellState {
|
|
12
12
|
readonly input: EvalToolInput;
|
|
13
|
+
readonly runtime?: EvalRuntimeInfo;
|
|
13
14
|
readonly startedAt: number;
|
|
14
15
|
readonly signal: AbortSignal;
|
|
15
16
|
readonly onUpdate: AgentToolUpdateCallback<EvalToolDetails> | undefined;
|
|
@@ -125,6 +126,7 @@ export class CellResultBuilder {
|
|
|
125
126
|
return {
|
|
126
127
|
language: this.#state.input.language,
|
|
127
128
|
languages: [this.#state.input.language],
|
|
129
|
+
...(this.#state.runtime === undefined ? {} : { runtime: this.#state.runtime }),
|
|
128
130
|
...(this.#state.input.summary === undefined ? {} : { summary: this.#state.input.summary }),
|
|
129
131
|
durationMs: this.#state.durationMs,
|
|
130
132
|
wallDurationMs: Math.max(0, Date.now() - this.#state.startedAt),
|
|
@@ -139,9 +141,11 @@ export class CellResultBuilder {
|
|
|
139
141
|
...(this.#state.input.summary === undefined ? {} : { summary: this.#state.input.summary }),
|
|
140
142
|
code: this.#state.input.code,
|
|
141
143
|
language: this.#state.input.language,
|
|
144
|
+
...(this.#state.runtime === undefined ? {} : { runtime: this.#state.runtime }),
|
|
142
145
|
output: this.#state.output,
|
|
143
146
|
status: this.#state.status,
|
|
144
147
|
durationMs: this.#state.durationMs,
|
|
148
|
+
startedAt: this.#state.startedAt,
|
|
145
149
|
...(statusEvents === undefined ? {} : { statusEvents }),
|
|
146
150
|
...(output?.hasMarkdown ? { hasMarkdown: true } : {}),
|
|
147
151
|
},
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
EnabledEvalLanguages,
|
|
12
12
|
EvalInputSchema,
|
|
13
13
|
EvalKernelManager,
|
|
14
|
+
EvalRuntimes,
|
|
14
15
|
EvalToolDetails,
|
|
15
16
|
EvalToolInput,
|
|
16
17
|
ExecuteTool,
|
|
@@ -38,6 +39,8 @@ export interface CreateEvalToolOptions {
|
|
|
38
39
|
readonly spawnDefaultAgent?: string;
|
|
39
40
|
readonly modelId?: string;
|
|
40
41
|
readonly hostLine?: string;
|
|
42
|
+
/** Display identity of each language's runtime, shown in headers and details. */
|
|
43
|
+
readonly runtimes?: EvalRuntimes;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export interface EvalCellInvocation {
|
|
@@ -98,8 +98,10 @@ async function runEvalCell(
|
|
|
98
98
|
const bridgeAbortController = new AbortController();
|
|
99
99
|
const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
|
|
100
100
|
const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
|
|
101
|
+
const runtime = options.runtimes?.[invocation.input.language];
|
|
101
102
|
const state: CellState = {
|
|
102
103
|
input: invocation.input,
|
|
104
|
+
...(runtime === undefined ? {} : { runtime }),
|
|
103
105
|
startedAt: Date.now(),
|
|
104
106
|
signal: cellSignal,
|
|
105
107
|
onUpdate: invocation.onUpdate,
|