@oh-my-pi/pi-coding-agent 14.9.3 → 14.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 +96 -0
- package/package.json +7 -7
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/cli/setup-cli.ts +14 -161
- package/src/cli/stats-cli.ts +56 -2
- package/src/cli.ts +0 -1
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/settings-schema.ts +1 -11
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/eval.lark +30 -10
- package/src/eval/js/context-manager.ts +334 -601
- package/src/eval/js/shared/helpers.ts +237 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -2
- package/src/eval/js/shared/rewrite-imports.ts +211 -0
- package/src/eval/js/shared/runtime.ts +168 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +2 -4
- package/src/eval/js/worker-core.ts +146 -0
- package/src/eval/js/worker-entry.ts +24 -0
- package/src/eval/js/worker-protocol.ts +41 -0
- package/src/eval/parse.ts +218 -49
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +97 -96
- package/src/eval/py/index.ts +2 -2
- package/src/eval/py/kernel.ts +472 -900
- package/src/eval/py/prelude.py +106 -87
- package/src/eval/py/runner.py +879 -0
- package/src/eval/py/runtime.ts +3 -16
- package/src/eval/py/tool-bridge.ts +137 -0
- package/src/export/html/template.css +12 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +113 -7
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/internal-urls/agent-protocol.ts +63 -52
- package/src/internal-urls/artifact-protocol.ts +51 -51
- package/src/internal-urls/docs-index.generated.ts +35 -3
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +49 -7
- package/src/internal-urls/mcp-protocol.ts +2 -8
- package/src/internal-urls/memory-protocol.ts +89 -59
- package/src/internal-urls/router.ts +38 -22
- package/src/internal-urls/rule-protocol.ts +2 -20
- package/src/internal-urls/skill-protocol.ts +4 -27
- package/src/main.ts +1 -1
- package/src/mcp/manager.ts +17 -0
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/components/tree-selector.ts +4 -0
- package/src/modes/controllers/command-controller.ts +0 -23
- package/src/modes/controllers/event-controller.ts +23 -2
- package/src/modes/controllers/mcp-command-controller.ts +7 -10
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/theme/theme.ts +27 -27
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +14 -9
- package/src/prompts/commands/orchestrate.md +1 -0
- package/src/prompts/system/project-prompt.md +10 -2
- package/src/prompts/system/subagent-system-prompt.md +8 -8
- package/src/prompts/system/system-prompt.md +13 -7
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -10
- package/src/prompts/tools/eval.md +15 -30
- package/src/prompts/tools/github.md +6 -5
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/job.md +14 -6
- package/src/prompts/tools/task.md +20 -3
- package/src/registry/agent-registry.ts +2 -1
- package/src/sdk.ts +87 -89
- package/src/session/agent-session.ts +58 -21
- package/src/session/artifacts.ts +7 -4
- package/src/session/history-storage.ts +77 -19
- package/src/session/session-manager.ts +30 -1
- package/src/ssh/connection-manager.ts +32 -16
- package/src/ssh/sshfs-mount.ts +10 -7
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +14 -2
- package/src/task/index.ts +19 -5
- package/src/tool-discovery/tool-index.ts +21 -8
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +3 -2
- package/src/tools/bash.ts +15 -9
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +98 -7
- package/src/tools/browser/tab-worker.ts +104 -58
- package/src/tools/eval.ts +49 -11
- package/src/tools/fetch.ts +1 -1
- package/src/tools/gh.ts +140 -4
- package/src/tools/index.ts +12 -11
- package/src/tools/job.ts +48 -12
- package/src/tools/read.ts +5 -4
- package/src/tools/search.ts +3 -2
- package/src/tools/todo-write.ts +1 -1
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/web/search/index.ts +6 -4
- package/src/cli/jupyter-cli.ts +0 -106
- package/src/commands/jupyter.ts +0 -32
- package/src/eval/py/cancellation.ts +0 -28
- package/src/eval/py/gateway-coordinator.ts +0 -424
- package/src/internal-urls/jobs-protocol.ts +0 -120
- package/src/prompts/system/now-prompt.md +0 -7
- /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
import * as util from "node:util";
|
|
6
|
-
import * as vm from "node:vm";
|
|
7
|
-
|
|
8
|
-
import { parse as babelParse } from "@babel/parser";
|
|
9
|
-
import * as Diff from "diff";
|
|
1
|
+
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
10
2
|
import type { ToolSession } from "../../tools";
|
|
11
|
-
import { ToolError } from "../../tools/tool-errors";
|
|
12
|
-
import { JAVASCRIPT_PRELUDE_SOURCE } from "./prelude";
|
|
3
|
+
import { ToolAbortError, ToolError } from "../../tools/tool-errors";
|
|
13
4
|
import { callSessionTool, type JsStatusEvent } from "./tool-bridge";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
import { WorkerCore } from "./worker-core";
|
|
6
|
+
// Imported with `type: "file"` so Bun's bundler statically discovers the worker entry and
|
|
7
|
+
// embeds it inside `bun build --compile` single-file binaries. Mirrors the browser tab
|
|
8
|
+
// worker setup; see packages/coding-agent/src/tools/browser/tab-supervisor.ts for the
|
|
9
|
+
// rationale.
|
|
10
|
+
// @ts-expect-error -- Bun file-URL import attribute is not modeled by tsgo.
|
|
11
|
+
import jsWorkerEntryUrl from "./worker-entry.ts" with { type: "file" };
|
|
12
|
+
import type {
|
|
13
|
+
JsDisplayOutput,
|
|
14
|
+
RunErrorPayload,
|
|
15
|
+
SessionSnapshot,
|
|
16
|
+
Transport,
|
|
17
|
+
WorkerInbound,
|
|
18
|
+
WorkerOutbound,
|
|
19
|
+
} from "./worker-protocol";
|
|
20
|
+
|
|
21
|
+
export { rewriteStaticImports } from "./shared/rewrite-imports";
|
|
22
|
+
export type { JsDisplayOutput } from "./worker-protocol";
|
|
19
23
|
|
|
20
24
|
export interface VmRunState {
|
|
21
25
|
signal?: AbortSignal;
|
|
@@ -23,654 +27,383 @@ export interface VmRunState {
|
|
|
23
27
|
onDisplay?: (output: JsDisplayOutput) => void;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
interface
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
interface WorkerHandle {
|
|
31
|
+
mode: "worker" | "inline";
|
|
32
|
+
send(msg: WorkerInbound): void;
|
|
33
|
+
onMessage(handler: (msg: WorkerOutbound) => void): () => void;
|
|
34
|
+
terminate(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PendingRun {
|
|
38
|
+
runId: string;
|
|
39
|
+
runState: VmRunState;
|
|
40
|
+
toolSession: ToolSession;
|
|
41
|
+
resolve(value: { value: unknown }): void;
|
|
42
|
+
reject(error: Error): void;
|
|
43
|
+
toolCalls: Map<string, AbortController>;
|
|
44
|
+
settled: boolean;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
interface
|
|
47
|
+
interface JsSession {
|
|
41
48
|
sessionKey: string;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
context: vm.Context;
|
|
46
|
-
env: Map<string, string>;
|
|
47
|
-
timers: Set<NodeJS.Timeout>;
|
|
48
|
-
intervals: Set<NodeJS.Timeout>;
|
|
49
|
-
currentRun?: VmRunState;
|
|
49
|
+
worker: WorkerHandle;
|
|
50
|
+
state: "alive" | "dead";
|
|
51
|
+
pending: Map<string, PendingRun>;
|
|
50
52
|
queue: Promise<void>;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
+
const sessions = new Map<string, JsSession>();
|
|
56
|
+
const READY_TIMEOUT_MS = 5_000;
|
|
55
57
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
export async function executeInVmContext(options: {
|
|
59
|
+
sessionKey: string;
|
|
60
|
+
sessionId: string;
|
|
61
|
+
cwd: string;
|
|
62
|
+
session: ToolSession;
|
|
63
|
+
reset?: boolean;
|
|
64
|
+
code: string;
|
|
65
|
+
filename: string;
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
runState: VmRunState;
|
|
68
|
+
}): Promise<{ value: unknown }> {
|
|
69
|
+
if (options.reset) {
|
|
70
|
+
await resetVmContext(options.sessionKey);
|
|
65
71
|
}
|
|
66
|
-
|
|
72
|
+
const session = await acquireSession(options.sessionKey, {
|
|
73
|
+
cwd: options.cwd,
|
|
74
|
+
sessionId: options.sessionId,
|
|
75
|
+
});
|
|
76
|
+
return await runQueued(session, () => runOnce(session, options));
|
|
67
77
|
}
|
|
68
78
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
export async function resetVmContext(sessionKey: string): Promise<void> {
|
|
80
|
+
const session = sessions.get(sessionKey);
|
|
81
|
+
if (!session) return;
|
|
82
|
+
sessions.delete(sessionKey);
|
|
83
|
+
await killSession(session, new ToolError("JS context reset"));
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
async function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const filePath = resolvePath(state, rawPath);
|
|
81
|
-
const file = Bun.file(filePath);
|
|
82
|
-
const info = await file.stat().catch(() => undefined);
|
|
83
|
-
if (!info) {
|
|
84
|
-
throw new ToolError(`File not found: ${filePath}`);
|
|
85
|
-
}
|
|
86
|
-
if (info.isDirectory()) {
|
|
87
|
-
throw new ToolError(`Directory paths are not supported by this helper: ${filePath}`);
|
|
88
|
-
}
|
|
89
|
-
return { filePath, file, size: info.size };
|
|
86
|
+
export async function disposeAllVmContexts(): Promise<void> {
|
|
87
|
+
const all = [...sessions.values()];
|
|
88
|
+
sessions.clear();
|
|
89
|
+
await Promise.all(all.map(session => killSession(session, new ToolError("JS context disposed"))));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return data.byteLength;
|
|
92
|
+
async function runQueued<T>(session: JsSession, work: () => Promise<T>): Promise<T> {
|
|
93
|
+
const previous = session.queue;
|
|
94
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
95
|
+
session.queue = promise;
|
|
96
|
+
try {
|
|
97
|
+
await previous;
|
|
98
|
+
} catch {
|
|
99
|
+
// Previous run's failure must not poison this one.
|
|
101
100
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
typeof value === "string" || value instanceof Blob || value instanceof ArrayBuffer || ArrayBuffer.isView(value)
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function emitText(state: VmContextState, text: string): void {
|
|
112
|
-
if (!text) return;
|
|
113
|
-
state.currentRun?.onText?.(text.endsWith("\n") ? text : `${text}\n`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function emitStatus(state: VmContextState, event: JsStatusEvent): void {
|
|
117
|
-
state.currentRun?.onDisplay?.({ type: "status", event });
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function displayValue(state: VmContextState, value: unknown): void {
|
|
121
|
-
if (value === undefined) return;
|
|
122
|
-
if (value && typeof value === "object") {
|
|
123
|
-
const record = value as Record<string, unknown>;
|
|
124
|
-
if (record.type === "image" && typeof record.data === "string" && typeof record.mimeType === "string") {
|
|
125
|
-
state.currentRun?.onDisplay?.({
|
|
126
|
-
type: "image",
|
|
127
|
-
data: record.data,
|
|
128
|
-
mimeType: record.mimeType,
|
|
129
|
-
});
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
state.currentRun?.onDisplay?.({
|
|
133
|
-
type: "json",
|
|
134
|
-
data: structuredClone(value),
|
|
135
|
-
});
|
|
136
|
-
return;
|
|
101
|
+
try {
|
|
102
|
+
return await work();
|
|
103
|
+
} finally {
|
|
104
|
+
resolve();
|
|
137
105
|
}
|
|
138
|
-
emitText(state, String(value));
|
|
139
106
|
}
|
|
140
107
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
108
|
+
async function runOnce(
|
|
109
|
+
session: JsSession,
|
|
110
|
+
options: {
|
|
111
|
+
sessionId: string;
|
|
112
|
+
cwd: string;
|
|
113
|
+
session: ToolSession;
|
|
114
|
+
code: string;
|
|
115
|
+
filename: string;
|
|
116
|
+
runState: VmRunState;
|
|
117
|
+
},
|
|
118
|
+
): Promise<{ value: unknown }> {
|
|
119
|
+
const runId = `r-${Snowflake.next()}`;
|
|
120
|
+
const { promise, resolve, reject } = Promise.withResolvers<{ value: unknown }>();
|
|
121
|
+
const pending: PendingRun = {
|
|
122
|
+
runId,
|
|
123
|
+
runState: options.runState,
|
|
124
|
+
toolSession: options.session,
|
|
125
|
+
resolve,
|
|
126
|
+
reject,
|
|
127
|
+
toolCalls: new Map(),
|
|
128
|
+
settled: false,
|
|
129
|
+
};
|
|
130
|
+
session.pending.set(runId, pending);
|
|
131
|
+
|
|
132
|
+
const onAbort = (): void => {
|
|
133
|
+
const reason = options.runState.signal?.reason;
|
|
134
|
+
const abortError = reasonToError(reason, "Execution aborted");
|
|
135
|
+
// Cancel any in-flight tool calls first.
|
|
136
|
+
for (const ctrl of pending.toolCalls.values()) ctrl.abort(abortError);
|
|
137
|
+
// Hard-kill the worker — only way to interrupt synchronous user code.
|
|
138
|
+
void killSessionFor(session, abortError);
|
|
157
139
|
};
|
|
158
|
-
}
|
|
159
140
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
state.intervals.delete(timer);
|
|
165
|
-
return;
|
|
141
|
+
if (options.runState.signal?.aborted) {
|
|
142
|
+
queueMicrotask(onAbort);
|
|
143
|
+
} else {
|
|
144
|
+
options.runState.signal?.addEventListener("abort", onAbort, { once: true });
|
|
166
145
|
}
|
|
167
|
-
clearTimeout(timer);
|
|
168
|
-
state.timers.delete(timer);
|
|
169
|
-
}
|
|
170
146
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
emitStatus(state, { op: "read", path: filePath, bytes: size, chars: text.length });
|
|
185
|
-
return text;
|
|
186
|
-
},
|
|
187
|
-
writeFile: async (rawPath: string, data: unknown): Promise<string> => {
|
|
188
|
-
if (!isWriteData(data)) {
|
|
189
|
-
throw new ToolError("write() expects string, Blob, ArrayBuffer, or TypedArray data");
|
|
190
|
-
}
|
|
191
|
-
const filePath = resolvePath(state, rawPath);
|
|
192
|
-
if (typeof data === "string" || data instanceof Blob || data instanceof ArrayBuffer) {
|
|
193
|
-
await Bun.write(filePath, data);
|
|
194
|
-
} else {
|
|
195
|
-
await Bun.write(filePath, new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
196
|
-
}
|
|
197
|
-
emitStatus(state, { op: "write", path: filePath, bytes: getDataSize(data) });
|
|
198
|
-
return filePath;
|
|
199
|
-
},
|
|
200
|
-
append: async (rawPath: string, content: string): Promise<string> => {
|
|
201
|
-
const target = resolvePath(state, rawPath);
|
|
202
|
-
await Bun.write(
|
|
203
|
-
target,
|
|
204
|
-
`${await Bun.file(target)
|
|
205
|
-
.text()
|
|
206
|
-
.catch(() => "")}${content}`,
|
|
207
|
-
);
|
|
208
|
-
emitStatus(state, {
|
|
209
|
-
op: "append",
|
|
210
|
-
path: target,
|
|
211
|
-
chars: content.length,
|
|
212
|
-
bytes: utf8Encoder.encode(content).byteLength,
|
|
213
|
-
});
|
|
214
|
-
return target;
|
|
215
|
-
},
|
|
216
|
-
sortText: (text: string, options: VmHelperOptions = {}): string => {
|
|
217
|
-
const lines = String(text).split(/\r?\n/);
|
|
218
|
-
const deduped = options.unique ? Array.from(new Set(lines)) : lines;
|
|
219
|
-
const sorted = deduped.sort((a, b) => a.localeCompare(b));
|
|
220
|
-
if (options.reverse) {
|
|
221
|
-
sorted.reverse();
|
|
222
|
-
}
|
|
223
|
-
const result = sorted.join("\n");
|
|
224
|
-
emitStatus(state, {
|
|
225
|
-
op: "sort",
|
|
226
|
-
lines: sorted.length,
|
|
227
|
-
reverse: options.reverse === true,
|
|
228
|
-
unique: options.unique === true,
|
|
229
|
-
});
|
|
230
|
-
return result;
|
|
231
|
-
},
|
|
232
|
-
uniqText: (text: string, options: VmHelperOptions = {}): string | Array<[number, string]> => {
|
|
233
|
-
const lines = String(text)
|
|
234
|
-
.split(/\r?\n/)
|
|
235
|
-
.filter(line => line.length > 0);
|
|
236
|
-
const groups: Array<[number, string]> = [];
|
|
237
|
-
for (const line of lines) {
|
|
238
|
-
const last = groups.at(-1);
|
|
239
|
-
if (last && last[1] === line) {
|
|
240
|
-
last[0] += 1;
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
groups.push([1, line]);
|
|
244
|
-
}
|
|
245
|
-
emitStatus(state, { op: "uniq", groups: groups.length, count_mode: options.count === true });
|
|
246
|
-
if (options.count) {
|
|
247
|
-
return groups;
|
|
248
|
-
}
|
|
249
|
-
return groups.map(([, line]) => line).join("\n");
|
|
250
|
-
},
|
|
251
|
-
counter: (items: string | string[], options: VmHelperOptions = {}): Array<[number, string]> => {
|
|
252
|
-
const values = Array.isArray(items) ? items : String(items).split(/\r?\n/).filter(Boolean);
|
|
253
|
-
const counts = new Map<string, number>();
|
|
254
|
-
for (const item of values) {
|
|
255
|
-
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
256
|
-
}
|
|
257
|
-
const entries = Array.from(counts.entries())
|
|
258
|
-
.map(([item, count]) => [count, item] as [number, string])
|
|
259
|
-
.sort((a, b) => (options.reverse === false ? a[0] - b[0] : b[0] - a[0]) || a[1].localeCompare(b[1]));
|
|
260
|
-
const limited = entries.slice(0, options.limit ?? entries.length);
|
|
261
|
-
emitStatus(state, { op: "counter", unique: counts.size, total: values.length, top: limited.slice(0, 10) });
|
|
262
|
-
return limited;
|
|
263
|
-
},
|
|
264
|
-
diff: async (rawA: string, rawB: string): Promise<string> => {
|
|
265
|
-
const fileA = resolvePath(state, rawA);
|
|
266
|
-
const fileB = resolvePath(state, rawB);
|
|
267
|
-
const [a, b] = await Promise.all([Bun.file(fileA).text(), Bun.file(fileB).text()]);
|
|
268
|
-
const result = Diff.createTwoFilesPatch(fileA, fileB, a, b, "", "", { context: 3 });
|
|
269
|
-
emitStatus(state, {
|
|
270
|
-
op: "diff",
|
|
271
|
-
file_a: fileA,
|
|
272
|
-
file_b: fileB,
|
|
273
|
-
identical: a === b,
|
|
274
|
-
preview: result.slice(0, 500),
|
|
275
|
-
});
|
|
276
|
-
return result;
|
|
277
|
-
},
|
|
278
|
-
tree: async (searchPath = ".", options: VmHelperOptions = {}): Promise<string> => {
|
|
279
|
-
const root = resolvePath(state, searchPath);
|
|
280
|
-
const maxDepth = options.maxDepth ?? 3;
|
|
281
|
-
const showHidden = options.hidden ?? false;
|
|
282
|
-
const lines: string[] = [`${root}/`];
|
|
283
|
-
let entryCount = 0;
|
|
284
|
-
const walk = async (dir: string, prefix: string, depth: number): Promise<void> => {
|
|
285
|
-
if (depth > maxDepth) return;
|
|
286
|
-
const entries = (await fs.promises.readdir(dir, { withFileTypes: true }))
|
|
287
|
-
.filter(entry => showHidden || !entry.name.startsWith("."))
|
|
288
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
289
|
-
for (let index = 0; index < entries.length; index++) {
|
|
290
|
-
const entry = entries[index];
|
|
291
|
-
const isLast = index === entries.length - 1;
|
|
292
|
-
const connector = isLast ? "└── " : "├── ";
|
|
293
|
-
const suffix = entry.isDirectory() ? "/" : "";
|
|
294
|
-
lines.push(`${prefix}${connector}${entry.name}${suffix}`);
|
|
295
|
-
entryCount += 1;
|
|
296
|
-
if (entry.isDirectory()) {
|
|
297
|
-
await walk(path.join(dir, entry.name), `${prefix}${isLast ? " " : "│ "}`, depth + 1);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
await walk(root, "", 1);
|
|
302
|
-
const result = lines.join("\n");
|
|
303
|
-
emitStatus(state, { op: "tree", path: root, entries: entryCount, preview: result.slice(0, 1000) });
|
|
304
|
-
return result;
|
|
305
|
-
},
|
|
306
|
-
run: async (
|
|
307
|
-
command: string,
|
|
308
|
-
options: VmHelperOptions = {},
|
|
309
|
-
): Promise<{ stdout: string; stderr: string; exit_code: number }> => {
|
|
310
|
-
const cwd = options.cwd ? resolvePath(state, options.cwd) : state.cwd;
|
|
311
|
-
const timeoutMs =
|
|
312
|
-
typeof options.timeoutMs === "number"
|
|
313
|
-
? options.timeoutMs
|
|
314
|
-
: typeof options.timeout === "number"
|
|
315
|
-
? options.timeout * 1000
|
|
316
|
-
: undefined;
|
|
317
|
-
const timeoutSignal =
|
|
318
|
-
typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
319
|
-
? AbortSignal.timeout(timeoutMs)
|
|
320
|
-
: undefined;
|
|
321
|
-
const signal =
|
|
322
|
-
state.currentRun?.signal && timeoutSignal
|
|
323
|
-
? AbortSignal.any([state.currentRun.signal, timeoutSignal])
|
|
324
|
-
: (state.currentRun?.signal ?? timeoutSignal);
|
|
325
|
-
const child = Bun.spawn(["bash", "-lc", command], {
|
|
326
|
-
cwd,
|
|
327
|
-
env: getMergedEnv(state),
|
|
328
|
-
stdout: "pipe",
|
|
329
|
-
stderr: "pipe",
|
|
330
|
-
signal,
|
|
331
|
-
});
|
|
332
|
-
const [stdout, stderr, exit_code] = await Promise.all([
|
|
333
|
-
new Response(child.stdout as ReadableStream<Uint8Array>).text(),
|
|
334
|
-
new Response(child.stderr as ReadableStream<Uint8Array>).text(),
|
|
335
|
-
child.exited,
|
|
336
|
-
]);
|
|
337
|
-
const output = `${stdout}${stderr}`.slice(0, 500);
|
|
338
|
-
emitStatus(state, { op: "run", cmd: command.slice(0, 120), code: exit_code, output });
|
|
339
|
-
return { stdout, stderr, exit_code };
|
|
340
|
-
},
|
|
341
|
-
env: (key?: string, value?: string): string | Record<string, string> | undefined => {
|
|
342
|
-
if (!key) {
|
|
343
|
-
const env = Object.fromEntries(Object.entries(getMergedEnv(state)).sort(([a], [b]) => a.localeCompare(b)));
|
|
344
|
-
emitStatus(state, { op: "env", count: Object.keys(env).length, keys: Object.keys(env).slice(0, 20) });
|
|
345
|
-
return env;
|
|
346
|
-
}
|
|
347
|
-
if (value !== undefined) {
|
|
348
|
-
state.env.set(key, value);
|
|
349
|
-
emitStatus(state, { op: "env", key, value, action: "set" });
|
|
350
|
-
return value;
|
|
351
|
-
}
|
|
352
|
-
const result = state.env.get(key) ?? Bun.env[key];
|
|
353
|
-
emitStatus(state, { op: "env", key, value: result, action: "get" });
|
|
354
|
-
return result;
|
|
355
|
-
},
|
|
356
|
-
};
|
|
147
|
+
try {
|
|
148
|
+
session.worker.send({
|
|
149
|
+
type: "run",
|
|
150
|
+
runId,
|
|
151
|
+
code: options.code,
|
|
152
|
+
filename: options.filename,
|
|
153
|
+
snapshot: { cwd: options.cwd, sessionId: options.sessionId },
|
|
154
|
+
});
|
|
155
|
+
return await promise;
|
|
156
|
+
} finally {
|
|
157
|
+
options.runState.signal?.removeEventListener("abort", onAbort);
|
|
158
|
+
session.pending.delete(runId);
|
|
159
|
+
}
|
|
357
160
|
}
|
|
358
161
|
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
cwd: () => cwd,
|
|
363
|
-
platform: process.platform,
|
|
364
|
-
release: Object.freeze({ ...process.release }),
|
|
365
|
-
version: process.version,
|
|
366
|
-
versions: Object.freeze({ ...process.versions }),
|
|
367
|
-
});
|
|
368
|
-
}
|
|
162
|
+
async function acquireSession(sessionKey: string, snapshot: SessionSnapshot): Promise<JsSession> {
|
|
163
|
+
const existing = sessions.get(sessionKey);
|
|
164
|
+
if (existing && existing.state === "alive") return existing;
|
|
369
165
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
sessionId: string,
|
|
373
|
-
cwd: string,
|
|
374
|
-
session: ToolSession,
|
|
375
|
-
): Promise<VmContextState> {
|
|
376
|
-
const state: VmContextState = {
|
|
166
|
+
const worker = await spawnJsWorker();
|
|
167
|
+
const session: JsSession = {
|
|
377
168
|
sessionKey,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
context: {} as vm.Context,
|
|
382
|
-
env: new Map(),
|
|
383
|
-
timers: new Set(),
|
|
384
|
-
intervals: new Set(),
|
|
169
|
+
worker,
|
|
170
|
+
state: "alive",
|
|
171
|
+
pending: new Map(),
|
|
385
172
|
queue: Promise.resolve(),
|
|
386
173
|
};
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
emitText(state, `${prefix}${formatConsoleArgs(args)}`);
|
|
402
|
-
},
|
|
403
|
-
__omp_display__: (value: unknown) => displayValue(state, value),
|
|
404
|
-
setTimeout: createTrackedTimeout(state, false),
|
|
405
|
-
setInterval: createTrackedTimeout(state, true),
|
|
406
|
-
clearTimeout: (timer?: NodeJS.Timeout) => clearTrackedTimeout(state, false, timer),
|
|
407
|
-
clearInterval: (timer?: NodeJS.Timeout) => clearTrackedTimeout(state, true, timer),
|
|
408
|
-
queueMicrotask,
|
|
409
|
-
URL,
|
|
410
|
-
URLSearchParams,
|
|
411
|
-
TextEncoder,
|
|
412
|
-
TextDecoder,
|
|
413
|
-
AbortController,
|
|
414
|
-
AbortSignal,
|
|
415
|
-
structuredClone,
|
|
416
|
-
crypto,
|
|
417
|
-
webcrypto: crypto,
|
|
418
|
-
performance,
|
|
419
|
-
atob,
|
|
420
|
-
btoa,
|
|
421
|
-
Buffer,
|
|
422
|
-
process: createProcessSubset(cwd),
|
|
423
|
-
require: buildRequire(cwd),
|
|
424
|
-
createRequire,
|
|
425
|
-
fs,
|
|
426
|
-
fetch,
|
|
427
|
-
Blob,
|
|
428
|
-
File,
|
|
429
|
-
Headers,
|
|
430
|
-
Request,
|
|
431
|
-
Response,
|
|
432
|
-
globalThis: undefined,
|
|
433
|
-
};
|
|
434
|
-
const context = vm.createContext(contextGlobals);
|
|
435
|
-
context.globalThis = context;
|
|
436
|
-
state.context = context;
|
|
437
|
-
vm.runInContext(JAVASCRIPT_PRELUDE_SOURCE, context, {
|
|
438
|
-
filename: "js-prelude.js",
|
|
439
|
-
importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
|
174
|
+
const { promise: readyPromise, resolve: resolveReady, reject: rejectReady } = Promise.withResolvers<void>();
|
|
175
|
+
let resolved = false;
|
|
176
|
+
const unsubscribe = worker.onMessage(msg => {
|
|
177
|
+
if (!resolved && msg.type === "ready") {
|
|
178
|
+
resolved = true;
|
|
179
|
+
resolveReady();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!resolved && msg.type === "init-failed") {
|
|
183
|
+
resolved = true;
|
|
184
|
+
rejectReady(errorFromPayload(msg.error));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
handleSessionMessage(session, msg);
|
|
440
188
|
});
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
cwd: string,
|
|
448
|
-
session: ToolSession,
|
|
449
|
-
): Promise<VmContextState> {
|
|
450
|
-
const existing = vmContexts.get(sessionKey);
|
|
451
|
-
if (existing) {
|
|
452
|
-
existing.cwd = cwd;
|
|
453
|
-
existing.sessionId = sessionId;
|
|
454
|
-
existing.session = session;
|
|
455
|
-
return existing;
|
|
189
|
+
try {
|
|
190
|
+
await raceWithTimeout(readyPromise, READY_TIMEOUT_MS, "Timed out initializing JS eval worker");
|
|
191
|
+
} catch (error) {
|
|
192
|
+
unsubscribe();
|
|
193
|
+
await worker.terminate().catch(() => undefined);
|
|
194
|
+
throw error;
|
|
456
195
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return
|
|
196
|
+
worker.send({ type: "init", snapshot });
|
|
197
|
+
sessions.set(sessionKey, session);
|
|
198
|
+
return session;
|
|
460
199
|
}
|
|
461
200
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
201
|
+
function handleSessionMessage(session: JsSession, msg: WorkerOutbound): void {
|
|
202
|
+
switch (msg.type) {
|
|
203
|
+
case "text": {
|
|
204
|
+
const pending = session.pending.get(msg.runId);
|
|
205
|
+
pending?.runState.onText?.(msg.chunk);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
case "display": {
|
|
209
|
+
const pending = session.pending.get(msg.runId);
|
|
210
|
+
pending?.runState.onDisplay?.(msg.output);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
case "tool-call":
|
|
214
|
+
void handleToolCall(session, msg);
|
|
215
|
+
return;
|
|
216
|
+
case "result":
|
|
217
|
+
settlePending(session, msg);
|
|
218
|
+
return;
|
|
219
|
+
case "log":
|
|
220
|
+
logWorkerMessage(msg);
|
|
221
|
+
return;
|
|
222
|
+
case "ready":
|
|
223
|
+
case "init-failed":
|
|
224
|
+
case "closed":
|
|
225
|
+
return;
|
|
469
226
|
}
|
|
470
|
-
state.intervals.clear();
|
|
471
|
-
state.currentRun = undefined;
|
|
472
227
|
}
|
|
473
228
|
|
|
474
|
-
async function
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
229
|
+
async function handleToolCall(session: JsSession, msg: Extract<WorkerOutbound, { type: "tool-call" }>): Promise<void> {
|
|
230
|
+
const pending = session.pending.get(msg.runId);
|
|
231
|
+
if (!pending) {
|
|
232
|
+
safeSend(session, {
|
|
233
|
+
type: "tool-reply",
|
|
234
|
+
id: msg.id,
|
|
235
|
+
reply: { ok: false, error: { message: "Run no longer active" } },
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const ctrl = new AbortController();
|
|
240
|
+
pending.toolCalls.set(msg.id, ctrl);
|
|
479
241
|
try {
|
|
480
|
-
|
|
242
|
+
const value = await callSessionTool(msg.name, msg.args, {
|
|
243
|
+
session: pending.toolSession,
|
|
244
|
+
signal: ctrl.signal,
|
|
245
|
+
emitStatus: (event: JsStatusEvent) => pending.runState.onDisplay?.({ type: "status", event }),
|
|
246
|
+
});
|
|
247
|
+
safeSend(session, { type: "tool-reply", id: msg.id, reply: { ok: true, value } });
|
|
248
|
+
} catch (error) {
|
|
249
|
+
safeSend(session, { type: "tool-reply", id: msg.id, reply: { ok: false, error: toErrorPayload(error) } });
|
|
481
250
|
} finally {
|
|
482
|
-
|
|
251
|
+
pending.toolCalls.delete(msg.id);
|
|
483
252
|
}
|
|
484
253
|
}
|
|
485
254
|
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
// string literals, template literals, or comments — common in codemods — stay intact.
|
|
496
|
-
|
|
497
|
-
type BabelImportDeclaration = {
|
|
498
|
-
type: "ImportDeclaration";
|
|
499
|
-
start: number;
|
|
500
|
-
end: number;
|
|
501
|
-
source: { value: string };
|
|
502
|
-
specifiers: ReadonlyArray<{
|
|
503
|
-
type: "ImportDefaultSpecifier" | "ImportNamespaceSpecifier" | "ImportSpecifier";
|
|
504
|
-
local: { name: string };
|
|
505
|
-
imported?: { type: "Identifier"; name: string } | { type: "StringLiteral"; value: string };
|
|
506
|
-
}>;
|
|
507
|
-
attributes?: ReadonlyArray<{
|
|
508
|
-
key: { type: "Identifier"; name: string } | { type: "StringLiteral"; value: string };
|
|
509
|
-
value: { value: string };
|
|
510
|
-
}>;
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
function buildDynamicImportCall(sourceLiteral: string, withClause: string | undefined): string {
|
|
514
|
-
return withClause ? `import(${sourceLiteral}, { with: ${withClause} })` : `import(${sourceLiteral})`;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function buildWithClause(node: BabelImportDeclaration): string | undefined {
|
|
518
|
-
const attrs = node.attributes;
|
|
519
|
-
if (!attrs || attrs.length === 0) return undefined;
|
|
520
|
-
const pairs = attrs.map(attr => {
|
|
521
|
-
const key = attr.key.type === "Identifier" ? attr.key.name : JSON.stringify(attr.key.value);
|
|
522
|
-
return `${key}: ${JSON.stringify(attr.value.value)}`;
|
|
523
|
-
});
|
|
524
|
-
return `{ ${pairs.join(", ")} }`;
|
|
255
|
+
function settlePending(session: JsSession, msg: Extract<WorkerOutbound, { type: "result" }>): void {
|
|
256
|
+
const pending = session.pending.get(msg.runId);
|
|
257
|
+
if (!pending || pending.settled) return;
|
|
258
|
+
pending.settled = true;
|
|
259
|
+
if (msg.ok) {
|
|
260
|
+
pending.resolve({ value: undefined });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
pending.reject(errorFromPayload(msg.error));
|
|
525
264
|
}
|
|
526
265
|
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const importCall = buildDynamicImportCall(sourceLiteral, withClause);
|
|
531
|
-
|
|
532
|
-
let defaultName: string | undefined;
|
|
533
|
-
let namespaceName: string | undefined;
|
|
534
|
-
const namedPairs: Array<[string, string]> = [];
|
|
535
|
-
for (const spec of node.specifiers) {
|
|
536
|
-
if (spec.type === "ImportDefaultSpecifier") {
|
|
537
|
-
defaultName = spec.local.name;
|
|
538
|
-
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
539
|
-
namespaceName = spec.local.name;
|
|
540
|
-
} else if (spec.type === "ImportSpecifier" && spec.imported) {
|
|
541
|
-
const imported = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
542
|
-
namedPairs.push([imported, spec.local.name]);
|
|
543
|
-
}
|
|
266
|
+
async function killSessionFor(session: JsSession, error: Error): Promise<void> {
|
|
267
|
+
if (sessions.get(session.sessionKey) === session) {
|
|
268
|
+
sessions.delete(session.sessionKey);
|
|
544
269
|
}
|
|
270
|
+
await killSession(session, error);
|
|
271
|
+
}
|
|
545
272
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
273
|
+
async function killSession(session: JsSession, error: Error): Promise<void> {
|
|
274
|
+
if (session.state === "dead") return;
|
|
275
|
+
session.state = "dead";
|
|
276
|
+
for (const pending of session.pending.values()) {
|
|
277
|
+
if (pending.settled) continue;
|
|
278
|
+
pending.settled = true;
|
|
279
|
+
for (const ctrl of pending.toolCalls.values()) ctrl.abort(error);
|
|
280
|
+
pending.reject(error);
|
|
550
281
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
if (namespaceName) return `const ${namespaceName} = await ${importCall};`;
|
|
555
|
-
if (defaultName) return `const ${defaultName} = (await ${importCall}).default;`;
|
|
556
|
-
return `await ${importCall};`;
|
|
282
|
+
session.pending.clear();
|
|
283
|
+
await session.worker.terminate().catch(() => undefined);
|
|
557
284
|
}
|
|
558
285
|
|
|
559
|
-
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
let ast: { program: { body: ReadonlyArray<{ type: string }> } };
|
|
286
|
+
function safeSend(session: JsSession, msg: WorkerInbound): void {
|
|
287
|
+
if (session.state !== "alive") return;
|
|
563
288
|
try {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
allowReturnOutsideFunction: true,
|
|
568
|
-
allowImportExportEverywhere: true,
|
|
569
|
-
allowNewTargetOutsideFunction: true,
|
|
570
|
-
allowSuperOutsideMethod: true,
|
|
571
|
-
allowUndeclaredExports: true,
|
|
572
|
-
errorRecovery: true,
|
|
573
|
-
}) as unknown as typeof ast;
|
|
574
|
-
} catch {
|
|
575
|
-
// Parser bailed entirely — let the VM surface the real syntax error.
|
|
576
|
-
return code;
|
|
289
|
+
session.worker.send(msg);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
logger.debug("js worker send failed", { error: err instanceof Error ? err.message : String(err) });
|
|
577
292
|
}
|
|
293
|
+
}
|
|
578
294
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
if (imports.length === 0) return code;
|
|
295
|
+
function reasonToError(reason: unknown, fallback: string): Error {
|
|
296
|
+
if (reason instanceof Error) return reason;
|
|
297
|
+
if (typeof reason === "string") return new ToolAbortError(reason);
|
|
298
|
+
return new ToolAbortError(fallback);
|
|
299
|
+
}
|
|
586
300
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
301
|
+
function errorFromPayload(payload: RunErrorPayload): Error {
|
|
302
|
+
if (payload.isAbort) {
|
|
303
|
+
const err = new ToolAbortError(payload.message || "Execution aborted");
|
|
304
|
+
if (payload.stack) err.stack = payload.stack;
|
|
305
|
+
return err;
|
|
592
306
|
}
|
|
593
|
-
|
|
307
|
+
const ctor = payload.isToolError ? ToolError : Error;
|
|
308
|
+
const error = new ctor(payload.message);
|
|
309
|
+
if (payload.name) error.name = payload.name;
|
|
310
|
+
if (payload.stack) error.stack = payload.stack;
|
|
311
|
+
return error;
|
|
594
312
|
}
|
|
595
313
|
|
|
596
|
-
function
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
314
|
+
function toErrorPayload(error: unknown): RunErrorPayload {
|
|
315
|
+
if (error instanceof Error) {
|
|
316
|
+
return {
|
|
317
|
+
name: error.name,
|
|
318
|
+
message: error.message,
|
|
319
|
+
stack: error.stack,
|
|
320
|
+
isAbort: error.name === "AbortError" || error.name === "ToolAbortError",
|
|
321
|
+
isToolError: error instanceof ToolError || error.name === "ToolError",
|
|
322
|
+
};
|
|
601
323
|
}
|
|
602
|
-
return {
|
|
603
|
-
source: `(async () => {\n${rewritten}\n})()`,
|
|
604
|
-
asyncWrapped: true,
|
|
605
|
-
};
|
|
324
|
+
return { message: String(error) };
|
|
606
325
|
}
|
|
607
326
|
|
|
608
|
-
|
|
609
|
-
if (
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const { promise
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
327
|
+
function logWorkerMessage(msg: Extract<WorkerOutbound, { type: "log" }>): void {
|
|
328
|
+
if (msg.level === "debug") logger.debug(msg.msg, msg.meta);
|
|
329
|
+
else if (msg.level === "warn") logger.warn(msg.msg, msg.meta);
|
|
330
|
+
else logger.error(msg.msg, msg.meta);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function raceWithTimeout<T>(promise: Promise<T>, timeoutMs: number, reason: string): Promise<T> {
|
|
334
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
335
|
+
const { promise: timeoutPromise, reject } = Promise.withResolvers<never>();
|
|
336
|
+
const onAbort = (): void => reject(new ToolError(reason));
|
|
337
|
+
timeoutSignal.addEventListener("abort", onAbort, { once: true });
|
|
338
|
+
try {
|
|
339
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
340
|
+
} finally {
|
|
341
|
+
timeoutSignal.removeEventListener("abort", onAbort);
|
|
620
342
|
}
|
|
621
|
-
const onAbort = () => reject(signal.reason ?? new Error("Execution aborted"));
|
|
622
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
623
|
-
promised.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
624
|
-
return promise;
|
|
625
343
|
}
|
|
626
344
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
runState: VmRunState;
|
|
637
|
-
}): Promise<{ value: unknown }> {
|
|
638
|
-
if (options.reset) {
|
|
639
|
-
await resetVmContext(options.sessionKey);
|
|
345
|
+
async function spawnJsWorker(): Promise<WorkerHandle> {
|
|
346
|
+
try {
|
|
347
|
+
const worker = new Worker(jsWorkerEntryUrl, { type: "module" });
|
|
348
|
+
return wrapBunWorker(worker);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
logger.warn("Bun Worker spawn failed; using inline JS eval worker (no sync-loop guard)", {
|
|
351
|
+
error: err instanceof Error ? err.message : String(err),
|
|
352
|
+
});
|
|
353
|
+
return spawnInlineWorker();
|
|
640
354
|
}
|
|
641
|
-
const state = await getOrCreateVmState(options.sessionKey, options.sessionId, options.cwd, options.session);
|
|
642
|
-
return runQueued(state, async () => {
|
|
643
|
-
state.currentRun = options.runState;
|
|
644
|
-
try {
|
|
645
|
-
if (options.runState.signal?.aborted) {
|
|
646
|
-
throw options.runState.signal.reason ?? new Error("Execution aborted");
|
|
647
|
-
}
|
|
648
|
-
const wrapped = wrapCode(options.code);
|
|
649
|
-
const value = vm.runInContext(wrapped.source, state.context, {
|
|
650
|
-
filename: options.filename,
|
|
651
|
-
timeout: options.timeoutMs,
|
|
652
|
-
importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
|
653
|
-
});
|
|
654
|
-
const awaited = await awaitMaybePromise(value, options.runState.signal);
|
|
655
|
-
displayValue(state, awaited);
|
|
656
|
-
return { value: awaited };
|
|
657
|
-
} finally {
|
|
658
|
-
state.currentRun = undefined;
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
355
|
}
|
|
662
356
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
357
|
+
function wrapBunWorker(worker: Worker): WorkerHandle {
|
|
358
|
+
return {
|
|
359
|
+
mode: "worker",
|
|
360
|
+
send(msg) {
|
|
361
|
+
worker.postMessage(msg);
|
|
362
|
+
},
|
|
363
|
+
onMessage(handler) {
|
|
364
|
+
const wrap = (event: MessageEvent): void => handler(event.data as WorkerOutbound);
|
|
365
|
+
worker.addEventListener("message", wrap);
|
|
366
|
+
return () => worker.removeEventListener("message", wrap);
|
|
367
|
+
},
|
|
368
|
+
async terminate() {
|
|
369
|
+
worker.terminate();
|
|
370
|
+
},
|
|
371
|
+
};
|
|
668
372
|
}
|
|
669
373
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Inline fallback for environments where Bun cannot spawn the worker entry
|
|
376
|
+
* (e.g. some test runners). Preserves behavior but cannot interrupt synchronous
|
|
377
|
+
* infinite loops because user code runs on the main thread.
|
|
378
|
+
*/
|
|
379
|
+
function spawnInlineWorker(): WorkerHandle {
|
|
380
|
+
const hostListeners = new Set<(message: WorkerOutbound) => void>();
|
|
381
|
+
const workerListeners = new Set<(message: WorkerInbound) => void>();
|
|
382
|
+
const workerTransport: Transport = {
|
|
383
|
+
send: msg =>
|
|
384
|
+
queueMicrotask(() => {
|
|
385
|
+
for (const listener of hostListeners) listener(msg);
|
|
386
|
+
}),
|
|
387
|
+
onMessage: handler => {
|
|
388
|
+
workerListeners.add(handler);
|
|
389
|
+
return () => workerListeners.delete(handler);
|
|
390
|
+
},
|
|
391
|
+
close: () => {},
|
|
392
|
+
};
|
|
393
|
+
new WorkerCore(workerTransport);
|
|
394
|
+
return {
|
|
395
|
+
mode: "inline",
|
|
396
|
+
send: msg =>
|
|
397
|
+
queueMicrotask(() => {
|
|
398
|
+
for (const listener of workerListeners) listener(msg);
|
|
399
|
+
}),
|
|
400
|
+
onMessage: handler => {
|
|
401
|
+
hostListeners.add(handler);
|
|
402
|
+
return () => hostListeners.delete(handler);
|
|
403
|
+
},
|
|
404
|
+
async terminate() {
|
|
405
|
+
hostListeners.clear();
|
|
406
|
+
workerListeners.clear();
|
|
407
|
+
},
|
|
408
|
+
};
|
|
676
409
|
}
|