@code-yeongyu/senpi-codemode 2026.9.30 → 2026.10.1-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +11 -0
- package/package.json +4 -4
- package/src/extension/runtime-factory.ts +13 -2
- package/src/kernels/py/kernel-contract.ts +4 -0
- package/src/kernels/py/kernel.ts +3 -2
- package/src/kernels/py/prelude.py +11 -1
- package/src/kernels/py/startup.ts +75 -0
- package/src/kernels/py/transport.ts +12 -11
- package/src/prompt/eval-prompt-template.ts +1 -1
- package/src/skill/bun-1-4/SKILL.md +1 -1
- package/src/tool/cell-handler.ts +10 -14
- package/src/tool/render.ts +8 -5
- package/src/tool/run-eval-cell.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,50 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.10.1-3] - 2026-10-01
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The `eval` tool points at the bun-1-4 skill before a cell that installs a package, spawns a server or PTY, or starts a long run, instead of demanding it before the first JavaScript cell ([#2505](https://github.com/code-yeongyu/senpi/issues/2505)).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Kernel-originated tool approvals now reach the RPC client that submitted the cell, including approvals requested after a cell detaches and its original turn ends. Reusing a kernel no longer sends permission dialogs to its creation context. ([#2512](https://github.com/code-yeongyu/senpi/issues/2512))
|
|
28
|
+
|
|
29
|
+
- Python eval startup waits for kernel readiness with advancing stage events instead of a five-second total deadline. Cold Windows imports can complete normally; a hung start identifies its stalled stage. ([#2452](https://github.com/code-yeongyu/senpi/issues/2452))
|
|
30
|
+
|
|
31
|
+
- Fixed the running eval spinner freezing between output updates. ([#2503](https://github.com/code-yeongyu/senpi/issues/2503))
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
|
|
35
|
+
## [2026.10.1-2] - 2026-10-01
|
|
36
|
+
|
|
37
|
+
### Breaking Changes
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
|
|
47
|
+
## [2026.10.1] - 2026-10-01
|
|
48
|
+
|
|
49
|
+
### Breaking Changes
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
15
59
|
## [2026.9.30] - 2026-09-30
|
|
16
60
|
|
|
17
61
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -18,6 +18,9 @@ task-tool names are known.
|
|
|
18
18
|
events, tool-call summaries, elapsed duration, and structured display state;
|
|
19
19
|
a terminal peek preserves the exact final result.
|
|
20
20
|
- Loopback, bearer-authenticated kernel bridge with bounded JSONL frames.
|
|
21
|
+
- Tool calls retain the submitting cell's host context, including its RPC approval
|
|
22
|
+
channel, even after detachment or kernel reuse. Headless sessions deny commands
|
|
23
|
+
that require approval rather than waiting for an absent UI.
|
|
21
24
|
- Structured status events for file operations, environment access, phases,
|
|
22
25
|
bridge activity, and delegated task progress.
|
|
23
26
|
- One versioned `senpi.eval.execution` event at terminal cell settlement. The
|
|
@@ -56,6 +59,14 @@ task-tool names are known.
|
|
|
56
59
|
A missing optional interpreter removes that language from the session's `eval`
|
|
57
60
|
schema; it is not an installation failure.
|
|
58
61
|
|
|
62
|
+
Python startup waits for the interpreter's `ready` event. It reports progress
|
|
63
|
+
through `stdlib-imports`, `runtime-init`, and `host-init`; advancing to the next
|
|
64
|
+
stage resets an inactivity guard rather than consuming a total startup budget.
|
|
65
|
+
The default guard is 11 seconds per stage, derived from a measured 5.220-second
|
|
66
|
+
Windows fresh-cache bootstrap p99 (30 samples). A stalled or failed start names
|
|
67
|
+
the last stage and retains the interpreter's diagnostic error. The low-level
|
|
68
|
+
`PythonKernel.start({ startupTimeoutMs })` override applies per stage.
|
|
69
|
+
|
|
59
70
|
### Session environment
|
|
60
71
|
|
|
61
72
|
Every kernel starts with the active session's `PI_*` environment — `PI_SESSION_ID`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.10.1-3",
|
|
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.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.1-3",
|
|
34
34
|
"typebox": "1.3.34"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.10.1-3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.10.1-3"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ExtensionContext, withBundledBunCommands } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
3
3
|
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
4
4
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
@@ -67,7 +67,7 @@ export async function createRuntime(
|
|
|
67
67
|
const executeTool = createExecuteTool(pi, activeTools);
|
|
68
68
|
const create = options.createSessionManager ?? createCodemodeSessionManager;
|
|
69
69
|
const sessionId = sessionIdFrom(event);
|
|
70
|
-
const sessionEnv = sessionEnvironmentFrom(ctx);
|
|
70
|
+
const sessionEnv = { ...sessionEnvironmentFrom(ctx), ...bundledBunPathEntry() };
|
|
71
71
|
const configuredPoolWidth = settings.parallelPoolWidth;
|
|
72
72
|
const parallelPoolWidth = Number.isFinite(configuredPoolWidth) ? Math.max(1, Math.trunc(configuredPoolWidth)) : 1;
|
|
73
73
|
const manager = await create({
|
|
@@ -105,6 +105,17 @@ export function createExecuteTool(pi: CodemodeRuntimeAPI, activeTools?: Readonly
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// In a compiled executable, Bun Shell runs `bun` as this executable unless PATH has one, which booted
|
|
109
|
+
// a second agent from `bun test` in a cell (omo#9362). Kernel children get the same bundled `bun`
|
|
110
|
+
// directory the bash tool's PATH carries.
|
|
111
|
+
function bundledBunPathEntry(): Record<string, string> {
|
|
112
|
+
const env = withBundledBunCommands(process.env);
|
|
113
|
+
if (env === process.env) return {};
|
|
114
|
+
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
115
|
+
const value = env[pathKey];
|
|
116
|
+
return value === undefined ? {} : { [pathKey]: value };
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
function sessionIdFrom(event: unknown): string {
|
|
109
120
|
if (typeof event === "object" && event !== null && "sessionId" in event && typeof event.sessionId === "string") {
|
|
110
121
|
return event.sessionId;
|
|
@@ -3,6 +3,7 @@ import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/p
|
|
|
3
3
|
import type { EvalKernelRunInput } from "../../tool/types.ts";
|
|
4
4
|
import type { SessionEnvironment } from "../session-env.ts";
|
|
5
5
|
import type { KernelSpawnProcess } from "./process.ts";
|
|
6
|
+
import type { PythonStartupStage } from "./startup.ts";
|
|
6
7
|
import type { PythonTransportResult } from "./transport.ts";
|
|
7
8
|
|
|
8
9
|
export interface PythonKernelStartOptions {
|
|
@@ -13,7 +14,10 @@ export interface PythonKernelStartOptions {
|
|
|
13
14
|
readonly env?: NodeJS.ProcessEnv;
|
|
14
15
|
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
15
16
|
readonly sessionEnv?: SessionEnvironment;
|
|
17
|
+
/** Python bootstrap inactivity guard per advancing stage, not a total readiness deadline. */
|
|
16
18
|
readonly startupTimeoutMs?: number;
|
|
19
|
+
/** Observes bootstrap control events without mixing them into cell output callbacks. */
|
|
20
|
+
readonly onStartupProgress?: (stage: PythonStartupStage) => void;
|
|
17
21
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
18
22
|
readonly spawnProcess?: KernelSpawnProcess;
|
|
19
23
|
/** Post-cell collection, notice, and ceiling thresholds sent on `init`; absent leaves memory unmanaged. */
|
package/src/kernels/py/kernel.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
// allow: SIZE_OK — one persistent Python lifecycle state machine owns queue, generation, and retirement.
|
|
1
2
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
2
3
|
import type { KernelToolsInvokeOptions } from "../js/kernel-tools-types.ts";
|
|
3
4
|
import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
|
|
4
5
|
import { KernelMemoryHost } from "../shared/kernel-memory-host.ts";
|
|
5
6
|
import { KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
|
|
6
7
|
import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
|
|
8
|
+
import { pythonStartupHangGuardMs } from "./startup.ts";
|
|
7
9
|
import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
|
|
8
10
|
|
|
9
11
|
export type { PythonKernelRunOptions, PythonKernelStartOptions } from "./kernel-contract.ts";
|
|
10
12
|
export type { KernelChild, KernelSpawnOptions, KernelSpawnProcess } from "./process.ts";
|
|
11
13
|
|
|
12
|
-
const startupTimeoutMs = 5_000;
|
|
13
14
|
const interruptEscalationMs = 5_000;
|
|
14
15
|
|
|
15
16
|
export class PythonKernel {
|
|
@@ -229,7 +230,7 @@ export class PythonKernel {
|
|
|
229
230
|
: (this.#active?.input.onMessage ?? this.#options.onMessage);
|
|
230
231
|
callback?.(message);
|
|
231
232
|
},
|
|
232
|
-
startupTimeoutMs: this.#options.startupTimeoutMs ??
|
|
233
|
+
startupTimeoutMs: this.#options.startupTimeoutMs ?? pythonStartupHangGuardMs,
|
|
233
234
|
isOwned: () => !this.#closed && generation === this.#generation,
|
|
234
235
|
onRetirementFailure: (transport, error) => {
|
|
235
236
|
if (!this.#transport) this.#transport = transport;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
# noqa: SIZE_OK — this dependency-free subprocess prelude must ship as one file.
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
# Emit before importing the stdlib graph: a cold interpreter can still be making
|
|
7
|
+
# progress after the former five-second total startup deadline.
|
|
8
|
+
sys.__stdout__.write('{"type":"status","event":{"op":"kernel-startup","stage":"stdlib-imports"}}\n')
|
|
9
|
+
sys.__stdout__.flush()
|
|
10
|
+
|
|
4
11
|
import ast
|
|
5
12
|
import asyncio # noqa: ANYIO_OK — stdlib-only embedded kernel runner.
|
|
6
13
|
import base64
|
|
@@ -15,7 +22,6 @@ import os
|
|
|
15
22
|
import re
|
|
16
23
|
import signal
|
|
17
24
|
import subprocess
|
|
18
|
-
import sys
|
|
19
25
|
import time
|
|
20
26
|
import traceback
|
|
21
27
|
import types
|
|
@@ -29,6 +35,9 @@ from threading import Lock, Thread
|
|
|
29
35
|
from typing import Any, Callable, Union
|
|
30
36
|
from urllib.parse import unquote
|
|
31
37
|
|
|
38
|
+
sys.__stdout__.write('{"type":"status","event":{"op":"kernel-startup","stage":"runtime-init"}}\n')
|
|
39
|
+
sys.__stdout__.flush()
|
|
40
|
+
|
|
32
41
|
CONNECTION: dict[str, Any] = {}
|
|
33
42
|
USER_NS: dict[str, Any] = {"__name__": "__main__", "__doc__": None, "__builtins__": __builtins__}
|
|
34
43
|
LOOP = asyncio.new_event_loop()
|
|
@@ -1463,6 +1472,7 @@ def _start_parent_watch() -> None:
|
|
|
1463
1472
|
|
|
1464
1473
|
|
|
1465
1474
|
def main() -> None:
|
|
1475
|
+
emit_status("kernel-startup", force=True, stage="host-init")
|
|
1466
1476
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
1467
1477
|
_start_parent_watch()
|
|
1468
1478
|
host_closed = False
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
|
|
3
|
+
// Fresh-cache Windows bootstrap: p99 5,220 ms across 30 samples in Actions run
|
|
4
|
+
// 36882163342. Twice that observation, rounded up to a second, is a hang guard,
|
|
5
|
+
// not a total startup budget: only an advancing kernel stage rearms it.
|
|
6
|
+
export const pythonStartupHangGuardMs = 11_000;
|
|
7
|
+
|
|
8
|
+
const stages = ["interpreter-launch", "stdlib-imports", "runtime-init", "host-init"] as const;
|
|
9
|
+
export type PythonStartupStage = (typeof stages)[number];
|
|
10
|
+
|
|
11
|
+
export class PythonKernelStartupError extends Error {
|
|
12
|
+
readonly stage: PythonStartupStage;
|
|
13
|
+
|
|
14
|
+
constructor(stage: PythonStartupStage, reason: string, cause?: Error) {
|
|
15
|
+
super(`Python kernel startup failed at ${stage}: ${reason}`, { cause });
|
|
16
|
+
this.name = "PythonKernelStartupError";
|
|
17
|
+
this.stage = stage;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Owns the ready event and a per-stage inactivity watchdog, never a total deadline. */
|
|
22
|
+
export class PythonStartup {
|
|
23
|
+
readonly #timeoutMs: number;
|
|
24
|
+
readonly #failureDetail: () => string;
|
|
25
|
+
readonly #ready = Promise.withResolvers<void>();
|
|
26
|
+
readonly ready = this.#ready.promise;
|
|
27
|
+
#timer: NodeJS.Timeout | undefined;
|
|
28
|
+
#stageIndex = 0;
|
|
29
|
+
#settled = false;
|
|
30
|
+
|
|
31
|
+
constructor(timeoutMs: number, failureDetail: () => string) {
|
|
32
|
+
this.#timeoutMs = timeoutMs;
|
|
33
|
+
this.#failureDetail = failureDetail;
|
|
34
|
+
this.#arm();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
progress(message: KernelToHostMessage): PythonStartupStage | undefined {
|
|
38
|
+
if (this.#settled || message.type !== "status" || message.event.op !== "kernel-startup") return;
|
|
39
|
+
const stage = stages.find((candidate) => candidate === message.event.stage);
|
|
40
|
+
if (stage === undefined) return;
|
|
41
|
+
const next = stages.indexOf(stage);
|
|
42
|
+
if (next <= this.#stageIndex) return;
|
|
43
|
+
this.#stageIndex = next;
|
|
44
|
+
this.#arm();
|
|
45
|
+
return stage;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
settle(error?: Error): boolean {
|
|
49
|
+
if (this.#settled) return false;
|
|
50
|
+
this.#settled = true;
|
|
51
|
+
clearTimeout(this.#timer);
|
|
52
|
+
if (error) {
|
|
53
|
+
const stage = stages[this.#stageIndex] ?? "interpreter-launch";
|
|
54
|
+
this.#ready.reject(
|
|
55
|
+
error instanceof PythonKernelStartupError
|
|
56
|
+
? error
|
|
57
|
+
: new PythonKernelStartupError(stage, error.message, error),
|
|
58
|
+
);
|
|
59
|
+
} else this.#ready.resolve();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#arm(): void {
|
|
64
|
+
clearTimeout(this.#timer);
|
|
65
|
+
this.#timer = setTimeout(() => {
|
|
66
|
+
const detail = this.#failureDetail().trim();
|
|
67
|
+
this.settle(
|
|
68
|
+
new PythonKernelStartupError(
|
|
69
|
+
stages[this.#stageIndex] ?? "interpreter-launch",
|
|
70
|
+
`no progress for ${this.#timeoutMs}ms${detail ? `; ${detail}` : ""}`,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}, this.#timeoutMs);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
splitCommand,
|
|
24
24
|
sweepProcessGroup,
|
|
25
25
|
waitForExit,
|
|
26
|
-
withTimeout,
|
|
27
26
|
} from "./process.ts";
|
|
27
|
+
import { PythonStartup, type PythonStartupStage } from "./startup.ts";
|
|
28
28
|
|
|
29
29
|
export type PythonTransportResult = Extract<KernelToHostMessage, { type: "result" }>;
|
|
30
30
|
|
|
@@ -44,6 +44,7 @@ export interface PythonTransportOptions {
|
|
|
44
44
|
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
45
45
|
readonly sessionEnv?: SessionEnvironment;
|
|
46
46
|
readonly startupTimeoutMs: number;
|
|
47
|
+
readonly onStartupProgress?: (stage: PythonStartupStage) => void;
|
|
47
48
|
readonly memory?: KernelMemoryThresholds;
|
|
48
49
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
49
50
|
readonly spawnProcess?: KernelSpawnProcess;
|
|
@@ -73,7 +74,7 @@ export class PythonKernelTransport {
|
|
|
73
74
|
readonly #child: KernelChild;
|
|
74
75
|
#stdoutBuffer = "";
|
|
75
76
|
#stderrTail = "";
|
|
76
|
-
#
|
|
77
|
+
#startup: PythonStartup | null = null;
|
|
77
78
|
#detachChildListeners: (() => void) | null = null;
|
|
78
79
|
#active = true;
|
|
79
80
|
#exited = false;
|
|
@@ -173,9 +174,8 @@ export class PythonKernelTransport {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
async #initialize(): Promise<void> {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
});
|
|
177
|
+
const startup = new PythonStartup(this.#options.startupTimeoutMs, () => this.#stderrTail);
|
|
178
|
+
this.#startup = startup;
|
|
179
179
|
const onStdout = (chunk: unknown) => this.#onStdout(String(chunk));
|
|
180
180
|
const onStderr = (chunk: unknown) => this.#onStderr(String(chunk));
|
|
181
181
|
const onError = (error: unknown) => this.#onError(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -192,7 +192,7 @@ export class PythonKernelTransport {
|
|
|
192
192
|
this.#child.on("exit", onExit);
|
|
193
193
|
const { sessionId, connection, memory } = this.#options;
|
|
194
194
|
this.#write({ type: "init", sessionId, connection, ...(memory === undefined ? {} : { memory }) });
|
|
195
|
-
await
|
|
195
|
+
await startup.ready;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
#write(message: HostToKernelMessage): void {
|
|
@@ -225,6 +225,11 @@ export class PythonKernelTransport {
|
|
|
225
225
|
}
|
|
226
226
|
if (!isKernelToHostMessage(decoded.message)) return;
|
|
227
227
|
const message = decoded.message;
|
|
228
|
+
if (message.type === "status" && message.event.op === "kernel-startup") {
|
|
229
|
+
const stage = this.#startup?.progress(message);
|
|
230
|
+
if (stage !== undefined) this.#options.onStartupProgress?.(stage);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
228
233
|
if (message.type === "ready") this.#settleStartup();
|
|
229
234
|
else if (message.type === "init-failed") this.#settleStartup(new Error(message.error.message));
|
|
230
235
|
else if (message.type === "result") this.#options.onResult(this, message);
|
|
@@ -258,11 +263,7 @@ export class PythonKernelTransport {
|
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
#settleStartup(error?: Error): boolean {
|
|
261
|
-
|
|
262
|
-
if (!settle) return false;
|
|
263
|
-
this.#settleReady = null;
|
|
264
|
-
settle(error);
|
|
265
|
-
return true;
|
|
266
|
+
return this.#startup?.settle(error) ?? false;
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
|
|
@@ -29,7 +29,7 @@ Host: {{hostLine}} — cells execute here. Size \`parallel(thunks)\` pools to it
|
|
|
29
29
|
A cell that outlives the foreground window detaches and keeps running; further cells on the same language queue behind it in submission order on the same kernel (state is shared, execution is serial) while other languages run at once, up to {{maxDetachedCells}} background cells per session; each detached cell completes as one notification. Do not re-run a detached or queued cell; read, cancel or enumerate them with \`eval({ action: "peek" | "stop", cell_id })\` / \`eval({ action: "list" })\`; list shows live and recently settled cells across languages without consuming notifications. Its own execution time is capped at {{runBudgetSeconds}}s — queue time and time inside host tool calls are not charged; the hard limit stays wall-clock from submission; raise \`timeout\` only for a declared long run — and a killed js cell that cannot settle restarts its kernel with every global lost.
|
|
30
30
|
|
|
31
31
|
{{#if py}}Python runs on a live event loop: use top-level \`await\`; \`asyncio.run(…)\` raises.{{/if}}
|
|
32
|
-
{{#if js}}{{#if jsBun}}JS runs in-process on Bun {{jsVersion}}: top-level \`await\`/\`return\` work; \`Bun.*\` builtins available, including \`new Bun.WebView()\` — a headless browser (navigate/click/evaluate/screenshot) to reach for before \`curl\` or a browser CLI when a page needs JS, a login, or a screenshot. Shell out through \`Bun.$\` or \`Bun.spawn\`, never \`Bun.spawnSync\`: a synchronous child blocks the worker and cannot be interrupted.{{#if bunSkillPath}}
|
|
32
|
+
{{#if js}}{{#if jsBun}}JS runs in-process on Bun {{jsVersion}}: top-level \`await\`/\`return\` work; \`Bun.*\` builtins available, including \`new Bun.WebView()\` — a headless browser (navigate/click/evaluate/screenshot) to reach for before \`curl\` or a browser CLI when a page needs JS, a login, or a screenshot. Shell out through \`Bun.$\` or \`Bun.spawn\`, never \`Bun.spawnSync\`: a synchronous child blocks the worker and cannot be interrupted.{{#if bunSkillPath}} Before a cell that installs a package, spawns a server or PTY, or starts a long run, read the bun-1-4 skill at {{bunSkillPath}} — its builtins replace the npm packages you would otherwise install.{{/if}}{{else}}JS runs under Node.js worker: top-level \`await\`/\`return\` work; \`fetch\`/\`Buffer\` available.{{/if}}{{/if}}
|
|
33
33
|
{{#if rb}}Ruby: synchronous; helper options are keyword args{{#if spawns}} (e.g. \`output("id", limit: 2)\`){{/if}}; the last expression auto-displays unless it is \`nil\`, an assignment, or a definition (like IRB).{{/if}}
|
|
34
34
|
{{#if jl}}Julia: synchronous; helper options are standard keyword args{{#if spawns}} (e.g. \`output("id", limit=2)\`){{/if}}; the last expression auto-displays unless it is an assignment or a definition (like the Julia REPL).{{/if}}
|
|
35
35
|
On error, fix and re-run only the failing step; a normal error keeps state, while a timeout or stop message says whether the kernel restarted.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bun-1-4
|
|
3
|
-
description: "
|
|
3
|
+
description: "Read before a js eval cell that installs a package, spawns a server or PTY, or starts a long run, and before any bun -e, script, server, CLI, test, bundle, or package-management work: this session's eval js kernel runs Bun 1.4+ (this skill is present only when it does). Bun 1.4 ships builtins that replace 15+ npm deps — check here BEFORE installing sharp, puppeteer/playwright (scraping), marked, node-cron, node-pty, concurrently, serve-static, tar, json5, fast-xml-parser, string-width. Triggers: eval js, bun, Bun.serve, bun test, bun build, bun install, bun run, image resize, headless browser, markdown render, cron, PTY."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Bun 1.4 — Use the Builtins First
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import {
|
|
2
3
|
type AgentToolResult,
|
|
3
4
|
type ExtensionContext,
|
|
@@ -54,11 +55,18 @@ export class CellHandler {
|
|
|
54
55
|
readonly #state: CellState;
|
|
55
56
|
readonly #runtime: CellBridgeRuntime;
|
|
56
57
|
readonly #resultBuilder: CellResultBuilder;
|
|
58
|
+
readonly #dispatchContext: ReturnType<typeof AsyncLocalStorage.snapshot>;
|
|
57
59
|
|
|
58
60
|
constructor(kernel: EvalKernel, state: CellState, runtime: CellBridgeRuntime) {
|
|
59
61
|
this.#kernel = kernel;
|
|
60
62
|
this.#state = state;
|
|
61
63
|
this.#runtime = runtime;
|
|
64
|
+
// Construct in the submitting cell's host context; worker callbacks cannot supply it (#2512).
|
|
65
|
+
// Bind this cell's capability once; undefined clears any enclosing JS grant for non-JS cells.
|
|
66
|
+
this.#dispatchContext =
|
|
67
|
+
runtime.kernelTools === undefined
|
|
68
|
+
? kernelToolsStorage.exit(() => AsyncLocalStorage.snapshot())
|
|
69
|
+
: kernelToolsStorage.run(runtime.kernelTools, () => AsyncLocalStorage.snapshot());
|
|
62
70
|
const settings = runtime.settings.outputSink;
|
|
63
71
|
this.#resultBuilder = new CellResultBuilder({
|
|
64
72
|
state,
|
|
@@ -89,7 +97,8 @@ export class CellHandler {
|
|
|
89
97
|
this.#resultBuilder.display(message);
|
|
90
98
|
return;
|
|
91
99
|
case "tool-call": {
|
|
92
|
-
|
|
100
|
+
// A retained worker carries its creation context, not this cell's RPC connection.
|
|
101
|
+
const pending = this.#dispatchContext(() => this.#handleToolCall(message));
|
|
93
102
|
this.#state.pendingBridgeCalls.push(pending);
|
|
94
103
|
await pending;
|
|
95
104
|
return;
|
|
@@ -122,19 +131,6 @@ export class CellHandler {
|
|
|
122
131
|
return this.#resultBuilder.liveResult();
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
/**
|
|
126
|
-
* The worker's message loop fires outside the async context `run-eval-cell.ts` enters around the
|
|
127
|
-
* awaited run chain, so the capability has to be entered here — around the whole dispatch, including
|
|
128
|
-
* the reserved agent()/output() bridges where a host task tool resolves the parent's kernel tools —
|
|
129
|
-
* for exactly the duration of each host tool call this cell makes (#1754). Without a capability
|
|
130
|
-
* (py/rb/jl) the store stays empty and `ctx.kernelTools` remains undefined.
|
|
131
|
-
*/
|
|
132
|
-
async #dispatchToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
|
|
133
|
-
const kernelTools = this.#runtime.kernelTools;
|
|
134
|
-
if (kernelTools === undefined) return await this.#handleToolCall(message);
|
|
135
|
-
return await kernelToolsStorage.run(kernelTools, async () => await this.#handleToolCall(message));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
134
|
async #handleToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
|
|
139
135
|
const startedAt = Date.now();
|
|
140
136
|
const metric = createToolCallMetric(message.toolName, startedAt);
|
package/src/tool/render.ts
CHANGED
|
@@ -73,9 +73,9 @@ const TOOL_CALL_PREVIEW_COUNT = 5;
|
|
|
73
73
|
const TOOL_CALL_COLLAPSED_VISUAL_LINES = 4;
|
|
74
74
|
const TOOL_CALL_COLLAPSED_ERROR_CODE_POINTS = 512;
|
|
75
75
|
const TOOL_ERROR_OMISSION_MARKER = "[tool error omitted]";
|
|
76
|
-
const
|
|
76
|
+
const LIVE_RENDER_TICK_MS = 100;
|
|
77
77
|
// A live row repaints on every tick, so this many ticks without a render means the row is gone.
|
|
78
|
-
const LIVE_TICKER_MAX_IDLE_TICKS =
|
|
78
|
+
const LIVE_TICKER_MAX_IDLE_TICKS = 600;
|
|
79
79
|
|
|
80
80
|
class PlainTextComponent implements EvalRenderComponent {
|
|
81
81
|
#blocks: readonly RenderBlock[] = [];
|
|
@@ -92,7 +92,7 @@ class PlainTextComponent implements EvalRenderComponent {
|
|
|
92
92
|
* The host only animates tool rows for streaming args, `task`, and results carrying
|
|
93
93
|
* `details.progress`; an eval row matches none of them, so nothing repaints it between
|
|
94
94
|
* update events. While a cell is non-terminal this drives the repaint itself so the
|
|
95
|
-
* header's elapsed time
|
|
95
|
+
* header's spinner and elapsed time advance, with no tool updates or RPC traffic. Detached
|
|
96
96
|
* and terminal cards never arm it, and a ticker whose row stopped rendering (transcript
|
|
97
97
|
* rebuild, session switch) stops itself after LIVE_TICKER_MAX_IDLE_TICKS and rearms on
|
|
98
98
|
* the next render, so dropped rows cannot accumulate intervals.
|
|
@@ -115,7 +115,7 @@ class PlainTextComponent implements EvalRenderComponent {
|
|
|
115
115
|
|
|
116
116
|
#armTicker(): void {
|
|
117
117
|
if (this.#ticker !== undefined || this.#invalidate === undefined) return;
|
|
118
|
-
this.#ticker = setInterval(() => this.#tick(),
|
|
118
|
+
this.#ticker = setInterval(() => this.#tick(), LIVE_RENDER_TICK_MS);
|
|
119
119
|
this.#ticker.unref?.();
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -336,7 +336,10 @@ function cellElapsedMs(cell: EvalCellResult, environment: RenderEnvironment): nu
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string {
|
|
339
|
-
const presentation = cellPresentation(
|
|
339
|
+
const presentation = cellPresentation(
|
|
340
|
+
cell.status,
|
|
341
|
+
environment.spinnerFrame ?? Math.floor((cellElapsedMs(cell, environment) ?? 0) / LIVE_RENDER_TICK_MS),
|
|
342
|
+
);
|
|
340
343
|
const runtimeBadge = cell.runtime === undefined ? "" : ` (${formatRuntimeBadge(cell.language, cell.runtime)})`;
|
|
341
344
|
let header = `eval ${cell.language}${runtimeBadge} ${presentation.label} ${presentation.icon}`;
|
|
342
345
|
if (cell.queuedBehind !== undefined && cell.queuedBehind.length > 0)
|
|
@@ -207,8 +207,8 @@ async function executeCell(
|
|
|
207
207
|
};
|
|
208
208
|
try {
|
|
209
209
|
const kernel = await execution.wait(options.kernelManager.getKernel(invocation.input.language, onMessage));
|
|
210
|
-
// Computed before the handler so
|
|
211
|
-
//
|
|
210
|
+
// Computed before the handler so its construction-time snapshot captures this cell's capability.
|
|
211
|
+
// Worker messages later restore that snapshot before calling host tools (#1754, #2512).
|
|
212
212
|
const kernelTools = jsKernelTools(kernel, invocation.input.language);
|
|
213
213
|
const runBound = async (): Promise<AgentToolResult<EvalToolDetails>> => {
|
|
214
214
|
const queue = kernel.queueSnapshot();
|