@code-yeongyu/senpi-codemode 2026.9.5 → 2026.9.7
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 +57 -0
- package/README.md +20 -4
- package/package.json +4 -4
- package/src/bridge/reserved.ts +2 -0
- package/src/kernels/jl/kernel.ts +2 -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/run-queue.ts +18 -3
- package/src/kernels/js/worker-core.js +30 -0
- package/src/kernels/js/worker-runtime.js +16 -0
- package/src/kernels/js/worker-shell-capture.d.ts +9 -0
- package/src/kernels/js/worker-shell-capture.js +32 -6
- package/src/kernels/js/worker-slot.ts +106 -0
- package/src/kernels/js/worker-startup.ts +69 -0
- package/src/kernels/py/transport.ts +2 -2
- package/src/kernels/rb/kernel.ts +2 -2
- package/src/kernels/shared/runtime-asset.ts +40 -14
- 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/interrupt-note.ts +29 -15
- package/src/tool/types.ts +2 -0
|
@@ -16,6 +16,7 @@ export class JsWorkerRuntime {
|
|
|
16
16
|
#env = new Map();
|
|
17
17
|
#hooks = null;
|
|
18
18
|
#pendingDisplays = [];
|
|
19
|
+
#children = new Set();
|
|
19
20
|
|
|
20
21
|
constructor(options) {
|
|
21
22
|
this.#cwd = options.cwd;
|
|
@@ -41,10 +42,24 @@ export class JsWorkerRuntime {
|
|
|
41
42
|
return value;
|
|
42
43
|
} finally {
|
|
43
44
|
this.#pendingDisplays = [];
|
|
45
|
+
this.#children.clear();
|
|
44
46
|
this.#hooks = null;
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
interrupt() {
|
|
51
|
+
for (const child of this.#children) {
|
|
52
|
+
if (child.exitCode === null && child.signalCode === null) child.kill();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#trackChild(child) {
|
|
57
|
+
if (child === null || typeof child !== "object" || typeof child.kill !== "function") return;
|
|
58
|
+
this.#children.add(child);
|
|
59
|
+
const forget = () => this.#children.delete(child);
|
|
60
|
+
if (child.exited instanceof Promise) child.exited.then(forget, forget);
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
async #drainPendingDisplays() {
|
|
49
64
|
while (this.#pendingDisplays.length > 0) {
|
|
50
65
|
const pending = this.#pendingDisplays;
|
|
@@ -99,6 +114,7 @@ export class JsWorkerRuntime {
|
|
|
99
114
|
const restoreShellCapture = installShellCapture({
|
|
100
115
|
isActive: () => this.#hooks !== null,
|
|
101
116
|
emitText: (stream, data) => this.#emitText(stream, data),
|
|
117
|
+
onChild: (child) => this.#trackChild(child),
|
|
102
118
|
});
|
|
103
119
|
globalThis.__senpi_restore_console__ = () => {
|
|
104
120
|
console.log = originalLog;
|
|
@@ -2,9 +2,18 @@ export type ShellCaptureStream = "stdout" | "stderr";
|
|
|
2
2
|
|
|
3
3
|
export type ShellCaptureRestore = () => void;
|
|
4
4
|
|
|
5
|
+
export interface ShellCaptureChild {
|
|
6
|
+
readonly exitCode: number | null;
|
|
7
|
+
readonly signalCode: string | null;
|
|
8
|
+
readonly exited: Promise<number>;
|
|
9
|
+
kill(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export interface ShellCaptureOptions {
|
|
6
13
|
readonly isActive: () => boolean;
|
|
7
14
|
readonly emitText: (stream: ShellCaptureStream, data: string) => void;
|
|
15
|
+
/** Receives every `Bun.spawn` child created while a cell is active so the runtime can kill it on interrupt. */
|
|
16
|
+
readonly onChild?: (child: ShellCaptureChild) => void;
|
|
8
17
|
}
|
|
9
18
|
|
|
10
19
|
export function installShellCapture(options: ShellCaptureOptions): ShellCaptureRestore;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const SHELL_CONFIG_METHODS = ["env", "cwd", "nothrow", "throws"];
|
|
2
2
|
const SHELL_READ_METHODS = ["text", "json", "lines", "arrayBuffer", "bytes", "blob"];
|
|
3
|
+
// `true | ( … )` hands every command in the template an empty pipe as stdin. The worker thread shares
|
|
4
|
+
// the host process's fd 0 (the TUI's terminal), which Bun.$ would otherwise inherit, so a stdin
|
|
5
|
+
// reader would wait on the user's keyboard forever. The newline before `)` keeps a trailing comment
|
|
6
|
+
// from swallowing the closing paren; the Bun shell has no other stdin control (no `$.stdin`, no
|
|
7
|
+
// redirect on a subshell).
|
|
8
|
+
const STDIN_ISOLATION_HEAD = "true | (\n";
|
|
9
|
+
const STDIN_ISOLATION_TAIL = "\n)";
|
|
3
10
|
|
|
4
11
|
export function installShellCapture(options) {
|
|
5
12
|
const bun = globalThis.Bun;
|
|
@@ -20,8 +27,9 @@ function isBunRuntime(bun) {
|
|
|
20
27
|
|
|
21
28
|
function capturedShell(originalShell, options) {
|
|
22
29
|
const shell = (strings, ...expressions) => {
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
if (!options.isActive()) return originalShell(strings, ...expressions);
|
|
31
|
+
const promise = originalShell(isolateStdin(strings), ...expressions);
|
|
32
|
+
return captureShellPromise(promise, options.emitText);
|
|
25
33
|
};
|
|
26
34
|
for (const key of Object.keys(originalShell)) shell[key] = originalShell[key];
|
|
27
35
|
for (const method of SHELL_CONFIG_METHODS) {
|
|
@@ -33,6 +41,18 @@ function capturedShell(originalShell, options) {
|
|
|
33
41
|
return shell;
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
function isolateStdin(strings) {
|
|
45
|
+
if (!Array.isArray(strings) || !Array.isArray(strings.raw)) return strings;
|
|
46
|
+
const cooked = [...strings];
|
|
47
|
+
const raw = [...strings.raw];
|
|
48
|
+
const last = cooked.length - 1;
|
|
49
|
+
cooked[0] = `${STDIN_ISOLATION_HEAD}${cooked[0]}`;
|
|
50
|
+
raw[0] = `${STDIN_ISOLATION_HEAD}${raw[0]}`;
|
|
51
|
+
cooked[last] = `${cooked[last]}${STDIN_ISOLATION_TAIL}`;
|
|
52
|
+
raw[last] = `${raw[last]}${STDIN_ISOLATION_TAIL}`;
|
|
53
|
+
return Object.freeze(Object.assign(cooked, { raw: Object.freeze(raw) }));
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
function captureShellPromise(promise, emitText) {
|
|
37
57
|
const prototype = Object.getPrototypeOf(promise);
|
|
38
58
|
let echo = true;
|
|
@@ -87,13 +107,19 @@ function capturedSpawn(originalSpawn, options) {
|
|
|
87
107
|
return (...args) => {
|
|
88
108
|
if (!options.isActive()) return originalSpawn(...args);
|
|
89
109
|
const [first, second] = args;
|
|
110
|
+
let child;
|
|
90
111
|
if (Array.isArray(first)) {
|
|
91
112
|
const spawnOptions = second === undefined ? {} : second;
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
child = needsStderrCapture(spawnOptions)
|
|
114
|
+
? drainStderr(originalSpawn(first, { ...spawnOptions, stderr: "pipe" }), options.emitText)
|
|
115
|
+
: originalSpawn(...args);
|
|
116
|
+
} else {
|
|
117
|
+
child = needsStderrCapture(first)
|
|
118
|
+
? drainStderr(originalSpawn({ ...first, stderr: "pipe" }), options.emitText)
|
|
119
|
+
: originalSpawn(...args);
|
|
94
120
|
}
|
|
95
|
-
|
|
96
|
-
return
|
|
121
|
+
options.onChild?.(child);
|
|
122
|
+
return child;
|
|
97
123
|
};
|
|
98
124
|
}
|
|
99
125
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { WorkerLike } from "./inline-worker.ts";
|
|
3
|
+
import { retireWorker, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
4
|
+
import type { JavaScriptKernelMode } from "./kernel-contract.ts";
|
|
5
|
+
import type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
6
|
+
import { WorkerStartupCancelledError } from "./worker-host.ts";
|
|
7
|
+
import { startWorkerWithInlineFallback } from "./worker-startup.ts";
|
|
8
|
+
|
|
9
|
+
export interface WorkerSlotListeners {
|
|
10
|
+
isOpen(): boolean;
|
|
11
|
+
onMessage(message: KernelToHostMessage): void;
|
|
12
|
+
onCrash(error: Error): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The kernel's current worker generation: startup with inline fallback, message fencing, bounded retirement. */
|
|
16
|
+
export class WorkerSlot {
|
|
17
|
+
readonly #options: JavaScriptKernelOptions;
|
|
18
|
+
readonly #listeners: WorkerSlotListeners;
|
|
19
|
+
#worker: WorkerLike | null = null;
|
|
20
|
+
#mode: JavaScriptKernelMode = "worker";
|
|
21
|
+
#generation = 0;
|
|
22
|
+
#ready: Promise<void> | null = null;
|
|
23
|
+
#startupAbort: AbortController | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(options: JavaScriptKernelOptions, listeners: WorkerSlotListeners) {
|
|
26
|
+
this.#options = options;
|
|
27
|
+
this.#listeners = listeners;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get mode(): JavaScriptKernelMode {
|
|
31
|
+
return this.#mode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get present(): boolean {
|
|
35
|
+
return this.#worker !== null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get startingUp(): boolean {
|
|
39
|
+
return this.#startupAbort !== null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
postMessage(message: HostToKernelMessage): void {
|
|
43
|
+
this.#worker?.postMessage(message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async ensureReady(): Promise<void> {
|
|
47
|
+
if (!this.#ready) {
|
|
48
|
+
const generation = ++this.#generation;
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
this.#startupAbort = controller;
|
|
51
|
+
const ready = startWorkerWithInlineFallback(
|
|
52
|
+
{
|
|
53
|
+
options: this.#options,
|
|
54
|
+
publish: (worker) => this.#publish(worker, generation),
|
|
55
|
+
isCurrent: (worker) => this.#isCurrent(worker, generation),
|
|
56
|
+
retire: (worker) => {
|
|
57
|
+
if (this.#worker === worker) this.#worker = null;
|
|
58
|
+
},
|
|
59
|
+
canFallBackInline: () => this.#listeners.isOpen() && generation === this.#generation,
|
|
60
|
+
},
|
|
61
|
+
controller.signal,
|
|
62
|
+
);
|
|
63
|
+
this.#ready = ready;
|
|
64
|
+
void ready.then(
|
|
65
|
+
() => {
|
|
66
|
+
if (this.#ready !== ready) return;
|
|
67
|
+
this.#startupAbort = null;
|
|
68
|
+
this.#mode = this.#worker?.mode ?? this.#mode;
|
|
69
|
+
},
|
|
70
|
+
() => {
|
|
71
|
+
if (this.#ready === ready) {
|
|
72
|
+
this.#ready = null;
|
|
73
|
+
this.#startupAbort = null;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return await this.#ready;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async retire(): Promise<WorkerRetirement> {
|
|
82
|
+
this.#generation += 1;
|
|
83
|
+
this.#startupAbort?.abort();
|
|
84
|
+
this.#startupAbort = null;
|
|
85
|
+
this.#ready = null;
|
|
86
|
+
const worker = this.#worker;
|
|
87
|
+
this.#worker = null;
|
|
88
|
+
if (!worker) return "terminated";
|
|
89
|
+
return await retireWorker(worker);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#publish(worker: WorkerLike, generation: number): void {
|
|
93
|
+
if (!this.#listeners.isOpen() || generation !== this.#generation) throw new WorkerStartupCancelledError();
|
|
94
|
+
this.#worker = worker;
|
|
95
|
+
worker.onMessage((message) => {
|
|
96
|
+
if (this.#isCurrent(worker, generation)) this.#listeners.onMessage(message);
|
|
97
|
+
});
|
|
98
|
+
worker.onError((error) => {
|
|
99
|
+
if (this.#isCurrent(worker, generation)) this.#listeners.onCrash(error);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#isCurrent(worker: WorkerLike, generation: number): boolean {
|
|
104
|
+
return this.#listeners.isOpen() && this.#worker === worker && this.#generation === generation;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
4
|
+
import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
|
|
5
|
+
import { type JavaScriptKernelOptions, localBridgeConnection } from "./local-module-loader.ts";
|
|
6
|
+
import { spawnNodeWorker, WorkerStartupCancelledError, waitForReady } from "./worker-host.ts";
|
|
7
|
+
|
|
8
|
+
export interface JavaScriptWorkerEntryUrlOptions extends CodemodeRuntimeAssetEnvironment {
|
|
9
|
+
readonly localPath?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveJsWorkerEntryUrl(options: JavaScriptWorkerEntryUrlOptions = {}): URL {
|
|
13
|
+
const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "worker-entry.js");
|
|
14
|
+
return pathToFileURL(requireCodemodeRuntimeAsset(localPath, join("kernels", "js", "worker-entry.js"), options));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkerStartupHooks {
|
|
18
|
+
readonly options: JavaScriptKernelOptions;
|
|
19
|
+
/** Wires the worker into the kernel; throws `WorkerStartupCancelledError` once the generation is stale. */
|
|
20
|
+
publish(worker: WorkerLike): void;
|
|
21
|
+
isCurrent(worker: WorkerLike): boolean;
|
|
22
|
+
retire(worker: WorkerLike): void;
|
|
23
|
+
canFallBackInline(): boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function startWorkerWithInlineFallback(hooks: WorkerStartupHooks, signal: AbortSignal): Promise<void> {
|
|
27
|
+
let worker = spawnWorker(hooks.options);
|
|
28
|
+
hooks.publish(worker);
|
|
29
|
+
try {
|
|
30
|
+
await initializeWorker(worker, hooks.options, signal);
|
|
31
|
+
return;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (!hooks.isCurrent(worker) || error instanceof WorkerStartupCancelledError) {
|
|
34
|
+
await worker.terminate();
|
|
35
|
+
throw new WorkerStartupCancelledError();
|
|
36
|
+
}
|
|
37
|
+
if (worker.mode === "inline") throw error;
|
|
38
|
+
hooks.retire(worker);
|
|
39
|
+
await worker.terminate();
|
|
40
|
+
}
|
|
41
|
+
if (!hooks.canFallBackInline()) throw new WorkerStartupCancelledError();
|
|
42
|
+
worker = createInlineWorker(hooks.options.cwd, hooks.options.parallelPoolWidth);
|
|
43
|
+
hooks.publish(worker);
|
|
44
|
+
await initializeWorker(worker, hooks.options, signal);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function spawnWorker(options: JavaScriptKernelOptions): WorkerLike {
|
|
48
|
+
try {
|
|
49
|
+
const url = options.workerEntryUrl ?? resolveJsWorkerEntryUrl();
|
|
50
|
+
return spawnNodeWorker(url, options.cwd, options.parallelPoolWidth);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (!(error instanceof Error)) throw error;
|
|
53
|
+
return createInlineWorker(options.cwd, options.parallelPoolWidth);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function initializeWorker(
|
|
58
|
+
worker: WorkerLike,
|
|
59
|
+
options: JavaScriptKernelOptions,
|
|
60
|
+
signal: AbortSignal,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const ready = waitForReady(worker, signal);
|
|
63
|
+
worker.postMessage({
|
|
64
|
+
type: "init",
|
|
65
|
+
sessionId: options.sessionId,
|
|
66
|
+
connection: localBridgeConnection(options),
|
|
67
|
+
});
|
|
68
|
+
await ready;
|
|
69
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
isKernelToHostMessage,
|
|
9
9
|
type KernelToHostMessage,
|
|
10
10
|
} from "../../bridge/protocol.ts";
|
|
11
|
-
import { type CodemodeRuntimeAssetEnvironment,
|
|
11
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
12
12
|
import {
|
|
13
13
|
defaultSpawn,
|
|
14
14
|
hardKill,
|
|
@@ -53,7 +53,7 @@ export interface PythonPreludePathOptions extends CodemodeRuntimeAssetEnvironmen
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function resolvePythonPreludePath(options: PythonPreludePathOptions = {}): string {
|
|
56
|
-
return
|
|
56
|
+
return requireCodemodeRuntimeAsset(
|
|
57
57
|
options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "prelude.py"),
|
|
58
58
|
join("kernels", "py", "prelude.py"),
|
|
59
59
|
options,
|
package/src/kernels/rb/kernel.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
|
-
import { type CodemodeRuntimeAssetEnvironment,
|
|
3
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
4
4
|
import { SubprocessKernel, type SubprocessSpawn } from "../shared/subprocess-kernel.ts";
|
|
5
5
|
|
|
6
6
|
export interface RubyKernelStartOptions {
|
|
@@ -17,7 +17,7 @@ export interface RubyRunnerPathOptions extends CodemodeRuntimeAssetEnvironment {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function resolveRubyRunnerPath(options: RubyRunnerPathOptions = {}): string {
|
|
20
|
-
return
|
|
20
|
+
return requireCodemodeRuntimeAsset(
|
|
21
21
|
options.localPath ?? join(import.meta.dirname, "runner.rb"),
|
|
22
22
|
join("kernels", "rb", "runner.rb"),
|
|
23
23
|
options,
|
|
@@ -6,26 +6,52 @@ export interface CodemodeRuntimeAssetEnvironment {
|
|
|
6
6
|
readonly executablePath?: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export class CodemodeRuntimeAssetMissingError extends Error {
|
|
10
|
+
readonly packageRelativePath: string;
|
|
11
|
+
readonly localPath: string;
|
|
12
|
+
readonly sidecarPath: string;
|
|
13
|
+
readonly executablePath: string;
|
|
14
|
+
|
|
15
|
+
constructor(packageRelativePath: string, localPath: string, sidecarPath: string, executablePath: string) {
|
|
16
|
+
super(
|
|
17
|
+
`codemode runtime asset ${packageRelativePath} is unavailable: module-relative path ${localPath} is not readable and the codemode sidecar is missing beside the executable ${executablePath} (expected ${sidecarPath}). Ship node_modules/@code-yeongyu/senpi-codemode next to the executable.`,
|
|
18
|
+
);
|
|
19
|
+
this.name = "CodemodeRuntimeAssetMissingError";
|
|
20
|
+
this.packageRelativePath = packageRelativePath;
|
|
21
|
+
this.localPath = localPath;
|
|
22
|
+
this.sidecarPath = sidecarPath;
|
|
23
|
+
this.executablePath = executablePath;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isBunVirtualPath(path: string): boolean {
|
|
28
|
+
return path.includes("/$bunfs/") || path.includes("~BUN") || path.includes("%7EBUN");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sidecarPathFor(packageRelativePath: string, executablePath: string): string {
|
|
32
|
+
return join(dirname(executablePath), "node_modules", "@code-yeongyu", "senpi-codemode", "src", packageRelativePath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function requireCodemodeRuntimeAsset(
|
|
36
|
+
localPath: string,
|
|
37
|
+
packageRelativePath: string,
|
|
38
|
+
{ executablePath = process.execPath }: CodemodeRuntimeAssetEnvironment = {},
|
|
39
|
+
): string {
|
|
40
|
+
const sidecarPath = sidecarPathFor(packageRelativePath, executablePath);
|
|
41
|
+
if (!isBunVirtualPath(localPath) && existsSync(localPath)) return localPath;
|
|
42
|
+
if (existsSync(sidecarPath)) return sidecarPath;
|
|
43
|
+
throw new CodemodeRuntimeAssetMissingError(packageRelativePath, localPath, sidecarPath, executablePath);
|
|
44
|
+
}
|
|
45
|
+
|
|
9
46
|
export function resolveCodemodeRuntimeAsset(
|
|
10
47
|
localPath: string,
|
|
11
48
|
packageRelativePath: string,
|
|
12
49
|
{ bunVersion = process.versions.bun, executablePath = process.execPath }: CodemodeRuntimeAssetEnvironment = {},
|
|
13
50
|
): string {
|
|
14
|
-
if (existsSync(localPath))
|
|
15
|
-
return localPath;
|
|
16
|
-
}
|
|
51
|
+
if (existsSync(localPath)) return localPath;
|
|
17
52
|
if (bunVersion) {
|
|
18
|
-
const sidecarPath =
|
|
19
|
-
|
|
20
|
-
"node_modules",
|
|
21
|
-
"@code-yeongyu",
|
|
22
|
-
"senpi-codemode",
|
|
23
|
-
"src",
|
|
24
|
-
packageRelativePath,
|
|
25
|
-
);
|
|
26
|
-
if (existsSync(sidecarPath)) {
|
|
27
|
-
return sidecarPath;
|
|
28
|
-
}
|
|
53
|
+
const sidecarPath = sidecarPathFor(packageRelativePath, executablePath);
|
|
54
|
+
if (existsSync(sidecarPath)) return sidecarPath;
|
|
29
55
|
}
|
|
30
56
|
return localPath;
|
|
31
57
|
}
|
|
@@ -77,9 +77,9 @@ const EVAL_PROMPT_TEMPLATE = `Run one step of code in a persistent kernel.
|
|
|
77
77
|
{{#if monitor}}- Start long-running work (build, test run, deploy, or watch) through \`tool.monitor({ command, filter })\`, putting the decisive-line filter inside the same cell, then keep working until its event wakes the turn.{{/if}}
|
|
78
78
|
</eval_first_batching>{{/if}}{{#if styleGpt}}<gpt_eval_dialect>
|
|
79
79
|
GPT eval: compose multi-tool work inside one cell with \`tool.<name>(args)\` and \`parallel(thunks)\`; do not split a planned step into serial tool calls.
|
|
80
|
-
-
|
|
80
|
+
{{#if monitor}}- A wait or a long run (build, test run, deploy, watch) starts through \`tool.monitor({ command, filter })\` in that same cell with the decisive-line filter; its event wakes the turn, so no cell sits on the wait and no child is spawned for it.
|
|
81
|
+
{{/if}}- Long cells detach on timeout and notify on completion; do not poll or re-run them.
|
|
81
82
|
- Filter, join, and aggregate tool results in the cell; return only decision-relevant facts.
|
|
82
|
-
{{#if monitor}}- For long-running build, test run, deploy, or watch work, start \`tool.monitor({ command, filter })\` with the decisive-line filter in the same cell; keep working until its event wakes the turn.{{/if}}
|
|
83
83
|
</gpt_eval_dialect>{{/if}}{{#if styleCodex}}Route multi-call steps through eval: one cell per step, independent lookups dispatched together via \`parallel(thunks)\`; keep work sequential only when one result determines the next action.
|
|
84
84
|
- Loop or comprehend over file sets with \`read()\`/stdlib instead of reading files one call at a time; post-process \`tool.<name>()\` results programmatically — filter, join, aggregate.
|
|
85
85
|
- Wrap failable calls in try/except inside the cell; a failed item degrades only itself. After two distinct failed strategies for the same fact, fall back to direct tool calls.
|
|
@@ -102,7 +102,7 @@ Host: {{hostLine}} — cells execute here. Size \`parallel(thunks)\` pools to it
|
|
|
102
102
|
A cell that outlives the foreground window detaches: it keeps its language kernel busy (another language can continue) and completes as one notification with its value or error and buffered output. Do not re-run a detached cell; read or cancel it with \`eval({ action: "peek", cell_id })\` / \`eval({ action: "stop", cell_id })\`.
|
|
103
103
|
|
|
104
104
|
{{#if py}}Python runs on a live event loop: use top-level \`await\`; \`asyncio.run(…)\` raises.{{/if}}
|
|
105
|
-
{{#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.{{#if bunSkillPath}} MUST READ the bun-1-4 skill at {{bunSkillPath}} before your first js cell — 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}}
|
|
105
|
+
{{#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, so a stop or timeout then loses every variable.{{#if bunSkillPath}} MUST READ the bun-1-4 skill at {{bunSkillPath}} before your first js cell — 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}}
|
|
106
106
|
{{#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}}
|
|
107
107
|
{{#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}}
|
|
108
108
|
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.
|
|
@@ -182,7 +182,7 @@ export function buildEvalPrompt(
|
|
|
182
182
|
description,
|
|
183
183
|
promptSnippet: "Run one incremental code cell in a persistent language kernel.",
|
|
184
184
|
promptGuidelines: [
|
|
185
|
-
BATCHING_GUIDELINES[style],
|
|
185
|
+
style === "gpt" && context.monitor === true ? GPT_MONITOR_BATCHING_GUIDELINE : BATCHING_GUIDELINES[style],
|
|
186
186
|
"Use eval reset only when a language kernel must be wiped; reset is scoped to the selected language.",
|
|
187
187
|
],
|
|
188
188
|
};
|
|
@@ -191,8 +191,13 @@ export function buildEvalPrompt(
|
|
|
191
191
|
/**
|
|
192
192
|
* System-prompt guideline per emphasis dialect. The default dialect carries
|
|
193
193
|
* maximum emphasis so unmapped models still batch through eval; the others are
|
|
194
|
-
* tuned to what steers that family reliably.
|
|
194
|
+
* tuned to what steers that family reliably. The GPT line routes waits to the
|
|
195
|
+
* subscription when `monitor` is reachable, because a GPT model that reads
|
|
196
|
+
* "long cells detach" as the way to wait awaits a `--watch` inside a cell.
|
|
195
197
|
*/
|
|
198
|
+
const GPT_MONITOR_BATCHING_GUIDELINE =
|
|
199
|
+
"Use eval to compose tool work in one cell; a wait or a long run starts through `tool.monitor` in that cell, so no cell sits on it and nothing polls.";
|
|
200
|
+
|
|
196
201
|
const BATCHING_GUIDELINES: Record<EvalEmphasisStyle, string> = {
|
|
197
202
|
default:
|
|
198
203
|
"**EVAL FIRST.** Any step needing MORE THAN ONE tool call MUST be ONE eval cell: run independent calls in parallel, wrap risky calls in try/except, and return distilled facts — NEVER a chain of single tool calls.",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IdleTimeout, type IdleTimeoutOptions, type TimeoutPauseHandle } from "../timeouts/idle-timeout.ts";
|
|
2
|
-
import type { EvalKernel } from "./types.ts";
|
|
2
|
+
import type { EvalKernel, KernelInterruptHandle } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
const INTERRUPT_DELIVERY_GRACE_MS = 100;
|
|
5
5
|
|
|
@@ -104,7 +104,8 @@ export class CellExecution {
|
|
|
104
104
|
this.#abort(this.#callerSignal.reason);
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
/** Resolves with the kernel's interrupt handle once the abort reached it; undefined when no kernel was bound. */
|
|
108
|
+
interruptHandle: Promise<KernelInterruptHandle> | undefined;
|
|
108
109
|
|
|
109
110
|
#abort(reason: unknown): void {
|
|
110
111
|
if (!this.#active) return;
|
|
@@ -118,15 +119,12 @@ export class CellExecution {
|
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
120
121
|
this.#interruptDeadline = setTimeout(() => this.#settleAbort(error), INTERRUPT_DELIVERY_GRACE_MS);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
() => this.#settleAbort(error),
|
|
128
|
-
(interruptError: unknown) => this.#settleAbort(interruptError),
|
|
129
|
-
);
|
|
122
|
+
const handle = Promise.resolve().then(async () => await kernel.interrupt(error.message));
|
|
123
|
+
this.interruptHandle = handle;
|
|
124
|
+
void handle.then(
|
|
125
|
+
() => this.#settleAbort(error),
|
|
126
|
+
(interruptError: unknown) => this.#settleAbort(interruptError),
|
|
127
|
+
);
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
#settleAbort(reason: unknown): void {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { WakeSourceState } from "../extension/wake-source-state.ts";
|
|
3
|
+
import type { EvalLanguage, EvalToolDetails } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export type EvalDetachedCellState = "running" | "detached" | "completed" | "failed" | "cancelled";
|
|
6
|
+
|
|
7
|
+
export interface EvalDetachedCellSnapshot {
|
|
8
|
+
readonly cellId: string;
|
|
9
|
+
readonly language: EvalLanguage;
|
|
10
|
+
readonly state: EvalDetachedCellState;
|
|
11
|
+
readonly outputTail: string;
|
|
12
|
+
readonly result: AgentToolResult<EvalToolDetails>;
|
|
13
|
+
readonly stateRetained: boolean | undefined;
|
|
14
|
+
/** Kernel-supplied detail about the interrupt outcome, e.g. an abandoned blocked worker. */
|
|
15
|
+
readonly interruptNote?: string;
|
|
16
|
+
/** Set only when the wall-clock kill deadline ended this cell. */
|
|
17
|
+
readonly hardLimitSeconds?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EvalDetachedCellNotification {
|
|
21
|
+
readonly cellId: string;
|
|
22
|
+
readonly content: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EvalDetachedCellNotifier {
|
|
26
|
+
notify(cells: readonly EvalDetachedCellNotification[]): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EvalDetachedCellStatusEntry {
|
|
30
|
+
readonly cellId: string;
|
|
31
|
+
readonly language: EvalLanguage;
|
|
32
|
+
readonly summary?: string;
|
|
33
|
+
readonly startedAtMs: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface EvalDetachedCellManagerOptions {
|
|
37
|
+
readonly artifactsDir?: string;
|
|
38
|
+
readonly notifier?: EvalDetachedCellNotifier;
|
|
39
|
+
/** Wall-clock kill deadline in seconds; defaults to the bash-parity 1800s. */
|
|
40
|
+
readonly hardLimitSeconds?: number;
|
|
41
|
+
readonly onStatusChange?: (entries: readonly EvalDetachedCellStatusEntry[]) => void;
|
|
42
|
+
/** Receives a full per-source liveness snapshot on every detached-cell transition; used by the goal builtin. */
|
|
43
|
+
readonly onWakeSourceState?: (state: WakeSourceState) => void;
|
|
44
|
+
readonly now?: () => number;
|
|
45
|
+
}
|