pi-ast-sgrep 2.0.2 → 2.2.0
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/README.md +16 -19
- package/dist/code-mode.d.ts +1 -1
- package/dist/code-mode.js +1 -1
- package/dist/codemode/connector.d.ts +18 -3
- package/dist/codemode/connector.js +85 -31
- package/dist/codemode/dispatch.d.ts +13 -1
- package/dist/codemode/dispatch.js +87 -24
- package/dist/codemode/guest-api.d.ts +16 -0
- package/dist/codemode/guest-api.js +194 -0
- package/dist/codemode/guest-worker.mjs +287 -0
- package/dist/codemode/index.d.ts +4 -3
- package/dist/codemode/index.js +4 -3
- package/dist/codemode/native.d.ts +1 -1
- package/dist/codemode/native.js +1 -1
- package/dist/codemode/runner.d.ts +13 -9
- package/dist/codemode/runner.js +411 -213
- package/dist/codemode/session-pool.d.ts +6 -1
- package/dist/codemode/session-pool.js +125 -32
- package/dist/codemode/types.d.ts +42 -2
- package/dist/codemode/types.js +40 -15
- package/dist/codemode/worker.d.ts +1 -1
- package/dist/codemode/worker.js +25 -2
- package/dist/host/commands.d.ts +6 -0
- package/dist/host/commands.js +49 -0
- package/dist/host/results.d.ts +123 -0
- package/dist/host/results.js +126 -0
- package/dist/host/tools.d.ts +28 -0
- package/dist/host/tools.js +802 -0
- package/dist/index.d.ts +7 -34
- package/dist/index.js +5 -543
- package/dist/runtime/config.d.ts +36 -0
- package/dist/runtime/config.js +98 -0
- package/dist/runtime/freshness.d.ts +43 -0
- package/dist/runtime/freshness.js +446 -0
- package/dist/runtime/index-health.d.ts +16 -0
- package/dist/runtime/index-health.js +111 -0
- package/dist/runtime/runtime.d.ts +48 -0
- package/dist/runtime/runtime.js +265 -0
- package/dist/runtime/sqlite.d.ts +15 -0
- package/dist/runtime/sqlite.js +63 -0
- package/dist/runtime/types.d.ts +55 -0
- package/dist/runtime/types.js +25 -0
- package/dist/ui/card.d.ts +66 -0
- package/dist/ui/card.js +375 -0
- package/dist/ui/present.d.ts +89 -0
- package/dist/ui/present.js +391 -0
- package/package.json +8 -7
- package/dist/codemode/sandbox-worker.d.ts +0 -1
- package/dist/codemode/sandbox-worker.js +0 -204
- package/dist/present.d.ts +0 -70
- package/dist/present.js +0 -260
- package/dist/runtime.d.ts +0 -137
- package/dist/runtime.js +0 -799
package/dist/codemode/runner.js
CHANGED
|
@@ -1,207 +1,434 @@
|
|
|
1
1
|
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { coerceHostArgs, normalizeCode, packGuestCall, resolveHostMethod, timeoutHint, unknownMethodError } from "./guest-api.js";
|
|
3
|
+
import { CODEMODE_HOST_METHODS } from "./types.js";
|
|
4
|
+
export { normalizeCode };
|
|
2
5
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
6
|
const MAX_CODE_CHARS = 32_000;
|
|
4
7
|
const MAX_BRIDGE_CALLS = 256;
|
|
5
8
|
const MAX_BRIDGE_REQUEST_CHARS = 64_000;
|
|
6
|
-
const MAX_BRIDGE_RESPONSE_CHARS = 4 * 1024 * 1024;
|
|
7
9
|
const MAX_ERROR_CHARS = 8_192;
|
|
8
10
|
const MAX_LOG_LINES = 100;
|
|
9
11
|
const MAX_LOG_CHARS = 64_000;
|
|
10
|
-
const MAX_LOG_LINE_CHARS = 4_096;
|
|
11
12
|
const MAX_RESULT_JSON_CHARS = 1_000_000;
|
|
12
|
-
const RESULT_SERIALIZE_TIMEOUT_MS = 1_000;
|
|
13
13
|
const MAX_TIMER_MS = 2_147_483_647;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
/** Hard ceiling on guest heap; an OOM guest kills its isolate, never the host. */
|
|
15
|
+
const WORKER_MAX_OLD_SPACE_MB = 512;
|
|
16
|
+
const WORKER_MAX_YOUNG_SPACE_MB = 64;
|
|
17
|
+
/** Parent-side RSS guard: a run may grow process RSS by this multiple of the heap cap. */
|
|
18
|
+
const MEMORY_SLACK = 1.5;
|
|
19
|
+
const MEMORY_POLL_MS = 50;
|
|
20
|
+
/** Grace for in-flight host calls after the guest reports done. */
|
|
21
|
+
const DRAIN_PENDING_MS = 250;
|
|
22
|
+
const WORKER_URL = new URL("./guest-worker.mjs", import.meta.url);
|
|
23
|
+
const ABORT_MESSAGE = "codemode timed out or aborted: pass timeoutMs to allow longer runs";
|
|
24
|
+
let idle = null;
|
|
25
|
+
let runSeq = 0;
|
|
26
|
+
const activeWorkers = new Set();
|
|
27
|
+
function spawnWorker() {
|
|
28
|
+
// Inline eval'd bootstrap: imports the real guest-worker file. execArgv is
|
|
29
|
+
// emptied so --import tsx / --test flags cannot leak in and crash the isolate.
|
|
30
|
+
const worker = new Worker("import(" + JSON.stringify(WORKER_URL.href) + ")", {
|
|
31
|
+
eval: true,
|
|
32
|
+
execArgv: [],
|
|
33
|
+
resourceLimits: {
|
|
34
|
+
maxOldGenerationSizeMb: WORKER_MAX_OLD_SPACE_MB,
|
|
35
|
+
maxYoungGenerationSizeMb: WORKER_MAX_YOUNG_SPACE_MB,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const handle = { worker, ready: Promise.resolve(), dead: false };
|
|
39
|
+
worker.on("error", () => { handle.dead = true; });
|
|
40
|
+
worker.on("exit", () => {
|
|
41
|
+
handle.dead = true;
|
|
42
|
+
if (idle === handle)
|
|
43
|
+
idle = null;
|
|
44
|
+
});
|
|
45
|
+
handle.ready = new Promise((resolve, reject) => {
|
|
46
|
+
const cleanup = () => {
|
|
47
|
+
worker.off("message", onMessage);
|
|
48
|
+
worker.off("error", onFail);
|
|
49
|
+
worker.off("exit", onFail);
|
|
50
|
+
};
|
|
51
|
+
const onMessage = (msg) => {
|
|
52
|
+
if (msg?.op !== "ready")
|
|
53
|
+
return;
|
|
54
|
+
cleanup();
|
|
55
|
+
resolve();
|
|
56
|
+
};
|
|
57
|
+
const onFail = (err) => {
|
|
58
|
+
cleanup();
|
|
59
|
+
reject(err instanceof Error ? err : new Error("codemode worker exited before ready (code " + err + ")"));
|
|
60
|
+
};
|
|
61
|
+
worker.on("message", onMessage);
|
|
62
|
+
worker.on("error", onFail);
|
|
63
|
+
worker.on("exit", onFail);
|
|
64
|
+
});
|
|
65
|
+
handle.ready.catch(() => undefined);
|
|
66
|
+
return handle;
|
|
24
67
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
function killWorker(handle) {
|
|
69
|
+
if (!handle)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (idle === handle)
|
|
72
|
+
idle = null;
|
|
73
|
+
handle.dead = true;
|
|
74
|
+
return handle.worker.terminate().then(() => undefined, () => undefined);
|
|
75
|
+
}
|
|
76
|
+
function acquireWorker() {
|
|
77
|
+
const candidate = idle;
|
|
78
|
+
idle = null;
|
|
79
|
+
const handle = candidate && !candidate.dead ? candidate : spawnWorker();
|
|
80
|
+
if (candidate && candidate.dead)
|
|
81
|
+
void killWorker(candidate);
|
|
82
|
+
handle.worker.ref?.();
|
|
83
|
+
// Pipeline the replacement while this run executes.
|
|
84
|
+
warmCodemodeSandbox().catch(() => undefined);
|
|
85
|
+
return handle;
|
|
86
|
+
}
|
|
87
|
+
/** Spawn the warm standby isolate (session_start / pre-call). */
|
|
88
|
+
export function warmCodemodeSandbox() {
|
|
89
|
+
if (idle && !idle.dead)
|
|
90
|
+
return idle.ready;
|
|
91
|
+
const handle = spawnWorker();
|
|
92
|
+
idle = handle;
|
|
93
|
+
void handle.ready.then(() => {
|
|
94
|
+
if (idle === handle)
|
|
95
|
+
handle.worker.unref?.();
|
|
96
|
+
}, () => undefined);
|
|
97
|
+
return handle.ready;
|
|
98
|
+
}
|
|
99
|
+
/** Drop the standby isolate and any in-flight runs (tests / session shutdown). */
|
|
100
|
+
export async function resetCodemodeSandboxForTests() {
|
|
101
|
+
await killWorker(idle);
|
|
102
|
+
await Promise.all([...activeWorkers].map((worker) => worker.terminate().catch(() => undefined)));
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Admission funnel: validate everything before any worker work happens.
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
function admitTimeout(requested) {
|
|
108
|
+
if (requested === undefined)
|
|
109
|
+
return { timeoutMs: DEFAULT_TIMEOUT_MS };
|
|
110
|
+
if (!Number.isFinite(requested) || requested <= 0)
|
|
111
|
+
return { error: "timeoutMs must be a positive finite number" };
|
|
112
|
+
return { timeoutMs: Math.min(MAX_TIMER_MS, Math.max(1, Math.trunc(requested))) };
|
|
113
|
+
}
|
|
114
|
+
function admitRun(code, timeoutMs, signal) {
|
|
115
|
+
if (code.length > MAX_CODE_CHARS)
|
|
116
|
+
return { error: "code exceeds " + MAX_CODE_CHARS + " characters; split into multiple asgrep calls" };
|
|
117
|
+
if (signal?.aborted)
|
|
118
|
+
return { error: "codemode aborted" };
|
|
119
|
+
const timeout = admitTimeout(timeoutMs);
|
|
120
|
+
if ("error" in timeout)
|
|
121
|
+
return timeout;
|
|
122
|
+
return { code: normalizeCode(code), timeoutMs: timeout.timeoutMs };
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// GuestRun: one run = one worker = one class instance. Explicit lifecycle
|
|
126
|
+
// states replace the closure flag soup: finished → terminal, accepting →
|
|
127
|
+
// messages still honored, completing → draining, aborting → user/timeout path.
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
class GuestRun {
|
|
130
|
+
code;
|
|
131
|
+
timeoutMs;
|
|
132
|
+
hostMethods;
|
|
133
|
+
signal;
|
|
134
|
+
statsFn;
|
|
135
|
+
runId;
|
|
136
|
+
finished = false;
|
|
137
|
+
accepting = true;
|
|
138
|
+
completing = false;
|
|
139
|
+
aborting = false;
|
|
140
|
+
handle;
|
|
141
|
+
pending = new Set();
|
|
142
|
+
logs = [];
|
|
143
|
+
logChars = 0;
|
|
144
|
+
callCount = 0;
|
|
145
|
+
lastMethod = null;
|
|
146
|
+
timer;
|
|
147
|
+
memTimer;
|
|
148
|
+
hostError;
|
|
149
|
+
wall0 = performance.now();
|
|
150
|
+
runController = new AbortController();
|
|
151
|
+
rssStart;
|
|
152
|
+
rssLimit;
|
|
153
|
+
resolve;
|
|
154
|
+
constructor(code, timeoutMs, hostMethods, signal, statsFn, runId) {
|
|
155
|
+
this.code = code;
|
|
156
|
+
this.timeoutMs = timeoutMs;
|
|
157
|
+
this.hostMethods = hostMethods;
|
|
158
|
+
this.signal = signal;
|
|
159
|
+
this.statsFn = statsFn;
|
|
160
|
+
this.runId = runId;
|
|
161
|
+
this.rssStart = process.memoryUsage().rss;
|
|
162
|
+
this.rssLimit = this.rssStart + WORKER_MAX_OLD_SPACE_MB * MEMORY_SLACK * 1_048_576;
|
|
84
163
|
}
|
|
85
|
-
|
|
86
|
-
return
|
|
164
|
+
wall() {
|
|
165
|
+
return performance.now() - this.wall0;
|
|
87
166
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
104
|
-
const fail = (error, logs = []) => {
|
|
105
|
-
finish(resultErr(error, logs, code, wall0, options.stats));
|
|
167
|
+
ok(result) {
|
|
168
|
+
const out = { ok: true, result, logs: this.logs, code: this.code, wallMs: this.wall() };
|
|
169
|
+
const stats = this.statsFn?.();
|
|
170
|
+
if (stats)
|
|
171
|
+
out.stats = stats;
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
err(error) {
|
|
175
|
+
const out = {
|
|
176
|
+
ok: false,
|
|
177
|
+
result: null,
|
|
178
|
+
logs: this.logs,
|
|
179
|
+
error: timeoutHint(error).slice(0, MAX_ERROR_CHARS),
|
|
180
|
+
code: this.code,
|
|
181
|
+
wallMs: this.wall(),
|
|
106
182
|
};
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
183
|
+
const stats = this.statsFn?.();
|
|
184
|
+
if (stats)
|
|
185
|
+
out.stats = stats;
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
cleanup() {
|
|
189
|
+
clearTimeout(this.timer);
|
|
190
|
+
clearInterval(this.memTimer);
|
|
191
|
+
this.signal?.removeEventListener("abort", this.onAbort);
|
|
192
|
+
if (this.handle) {
|
|
193
|
+
this.handle.worker.off("message", this.onMessage);
|
|
194
|
+
this.handle.worker.off("error", this.onError);
|
|
195
|
+
this.handle.worker.off("exit", this.onExit);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
finish(outcome) {
|
|
199
|
+
if (this.finished)
|
|
200
|
+
return;
|
|
201
|
+
this.finished = true;
|
|
202
|
+
this.accepting = false;
|
|
203
|
+
this.cleanup();
|
|
204
|
+
this.runController.abort();
|
|
205
|
+
if (this.handle) {
|
|
206
|
+
activeWorkers.delete(this.handle.worker);
|
|
207
|
+
void killWorker(this.handle);
|
|
208
|
+
}
|
|
209
|
+
this.resolve(outcome);
|
|
210
|
+
}
|
|
211
|
+
fail(message) {
|
|
212
|
+
this.finish(this.err(message));
|
|
213
|
+
}
|
|
214
|
+
abort() {
|
|
215
|
+
if (this.finished || this.aborting)
|
|
216
|
+
return;
|
|
217
|
+
this.aborting = true;
|
|
218
|
+
this.fail(ABORT_MESSAGE);
|
|
219
|
+
this.aborting = false;
|
|
220
|
+
}
|
|
221
|
+
onAbort = () => this.abort();
|
|
222
|
+
hostLog(line) {
|
|
223
|
+
if (this.logs.length >= MAX_LOG_LINES || this.logChars >= MAX_LOG_CHARS)
|
|
224
|
+
return;
|
|
225
|
+
const remaining = MAX_LOG_CHARS - this.logChars;
|
|
226
|
+
const bounded = line.length <= remaining ? line : line.slice(0, Math.max(0, remaining - 1)) + "…";
|
|
227
|
+
this.logs.push(bounded);
|
|
228
|
+
this.logChars += bounded.length;
|
|
229
|
+
}
|
|
230
|
+
async hostCall(method, payload) {
|
|
231
|
+
try {
|
|
232
|
+
if (this.runController.signal.aborted) {
|
|
233
|
+
throw Object.assign(new Error("codemode aborted"), { name: "AbortError" });
|
|
124
234
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
fail("codemode worker exceeded its bridge call allowance");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
receivedCallIds.add(call.id);
|
|
235
|
+
if (this.callCount >= MAX_BRIDGE_CALLS) {
|
|
236
|
+
throw new Error("codemode exceeds " + MAX_BRIDGE_CALLS + " host calls");
|
|
131
237
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
worker.once("exit", (code) => {
|
|
137
|
-
if (active)
|
|
138
|
-
fail(`codemode worker exited ${code}`);
|
|
139
|
-
});
|
|
140
|
-
const handleSandboxCall = async (call) => {
|
|
141
|
-
if (!active)
|
|
142
|
-
return;
|
|
143
|
-
let payload;
|
|
144
|
-
try {
|
|
145
|
-
if (call.payload.length > MAX_BRIDGE_REQUEST_CHARS) {
|
|
146
|
-
throw new Error(`codemode call arguments exceed ${MAX_BRIDGE_REQUEST_CHARS} characters`);
|
|
147
|
-
}
|
|
148
|
-
if (!Object.hasOwn(hostMethods, call.method)) {
|
|
149
|
-
throw new Error(`unknown asgrep method: ${call.method}`);
|
|
150
|
-
}
|
|
151
|
-
const input = JSON.parse(call.payload);
|
|
152
|
-
const methodCall = hostMethods[call.method];
|
|
153
|
-
const value = await methodCall(input, { signal: runController.signal });
|
|
154
|
-
payload = stringifyBounded({ ok: true, value }, MAX_BRIDGE_RESPONSE_CHARS, "codemode call result");
|
|
238
|
+
this.callCount += 1;
|
|
239
|
+
this.lastMethod = method;
|
|
240
|
+
if (payload.length > MAX_BRIDGE_REQUEST_CHARS) {
|
|
241
|
+
throw new Error("codemode call arguments exceed " + MAX_BRIDGE_REQUEST_CHARS + " characters");
|
|
155
242
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
error: safeErrorMessage(cause).slice(0, MAX_ERROR_CHARS),
|
|
160
|
-
});
|
|
243
|
+
const resolved = resolveHostMethod(method);
|
|
244
|
+
if (!resolved || !Object.hasOwn(this.hostMethods, resolved)) {
|
|
245
|
+
throw new Error(unknownMethodError(method));
|
|
161
246
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
247
|
+
const parsed = JSON.parse(payload);
|
|
248
|
+
const packed = Array.isArray(parsed.__guestArgs)
|
|
249
|
+
? packGuestCall(resolved, parsed.__guestArgs)
|
|
250
|
+
: parsed;
|
|
251
|
+
const input = coerceHostArgs(resolved, packed);
|
|
252
|
+
const invokeHost = this.hostMethods[resolved];
|
|
253
|
+
if (!invokeHost)
|
|
254
|
+
throw new Error(unknownMethodError(method));
|
|
255
|
+
const value = await invokeHost(input, { signal: this.runController.signal });
|
|
256
|
+
return JSON.stringify({ ok: true, value }, jsonSafe);
|
|
257
|
+
}
|
|
258
|
+
catch (cause) {
|
|
259
|
+
return JSON.stringify({ ok: false, error: safeErrorMessage(cause).slice(0, MAX_ERROR_CHARS) });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
respond(id, body) {
|
|
263
|
+
if (!this.handle || this.finished)
|
|
264
|
+
return;
|
|
265
|
+
try {
|
|
266
|
+
this.handle.worker.postMessage({ op: "call-result", runId: this.runId, id, body });
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Worker already terminated; the run is settled.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
onCallBatch(calls) {
|
|
273
|
+
for (const call of calls) {
|
|
274
|
+
const work = this.hostCall(call.method, call.payload)
|
|
275
|
+
.then((body) => this.respond(call.id, body))
|
|
276
|
+
.catch(() => this.respond(call.id, JSON.stringify({ ok: false, error: "codemode call failed" })));
|
|
277
|
+
this.pending.add(work);
|
|
278
|
+
void work.finally(() => this.pending.delete(work));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
onResult(msg) {
|
|
282
|
+
try {
|
|
283
|
+
const serialized = msg.serialized;
|
|
284
|
+
const result = serialized === undefined ? undefined : JSON.parse(serialized);
|
|
285
|
+
void this.complete(this.ok(result));
|
|
286
|
+
}
|
|
287
|
+
catch (cause) {
|
|
288
|
+
this.fail("codemode result decode failed: " + safeErrorMessage(cause));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
onMessage = (msg) => {
|
|
292
|
+
if (this.finished || !this.accepting || !msg || typeof msg !== "object")
|
|
293
|
+
return;
|
|
294
|
+
if (typeof msg.runId === "number" && msg.runId !== this.runId)
|
|
295
|
+
return;
|
|
296
|
+
if (msg.op === "log") {
|
|
297
|
+
this.hostLog(msg.line);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (msg.op === "call-batch") {
|
|
301
|
+
this.onCallBatch(Array.isArray(msg.calls) ? msg.calls : []);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (msg.op === "result") {
|
|
305
|
+
this.onResult(msg);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (msg.op === "error") {
|
|
309
|
+
this.fail(msg.error);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
onError = (cause) => {
|
|
313
|
+
void this.complete(this.err("codemode worker error: " + cause.message));
|
|
314
|
+
};
|
|
315
|
+
onExit = (exitCode) => {
|
|
316
|
+
void this.complete(this.err("codemode worker exited code=" + exitCode));
|
|
317
|
+
};
|
|
318
|
+
/** Give in-flight host calls a bounded settle window, then surface leaks. */
|
|
319
|
+
async drainPending(outcome) {
|
|
320
|
+
if (this.pending.size === 0)
|
|
321
|
+
return;
|
|
322
|
+
await Promise.race([
|
|
323
|
+
Promise.allSettled(this.pending),
|
|
324
|
+
new Promise((resolve) => setTimeout(resolve, DRAIN_PENDING_MS)),
|
|
325
|
+
]);
|
|
326
|
+
if (this.pending.size && outcome.ok) {
|
|
327
|
+
this.hostError ??= "program completed with a host call still running";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async complete(outcome) {
|
|
331
|
+
if (this.finished || this.completing)
|
|
332
|
+
return;
|
|
333
|
+
this.completing = true;
|
|
334
|
+
this.accepting = false;
|
|
335
|
+
await this.drainPending(outcome);
|
|
336
|
+
if (this.finished)
|
|
337
|
+
return;
|
|
338
|
+
this.finish(outcome.ok && this.hostError ? this.err(this.hostError) : outcome);
|
|
339
|
+
}
|
|
340
|
+
memoryLimitError() {
|
|
341
|
+
const now = process.memoryUsage().rss;
|
|
342
|
+
const deltaMb = Math.max(0, (now - this.rssStart) / 1_048_576);
|
|
343
|
+
const where = this.lastMethod ? " during " + this.lastMethod : "";
|
|
344
|
+
return ("codemode exceeded memory limit: process RSS +" + deltaMb.toFixed(1) + "MB in " +
|
|
345
|
+
Math.round(this.wall()) + "ms" + where + " (" + this.callCount + " host calls); " +
|
|
346
|
+
"worker heap cap is " + WORKER_MAX_OLD_SPACE_MB + "MB — split the program or stream less data");
|
|
347
|
+
}
|
|
348
|
+
async boot() {
|
|
349
|
+
try {
|
|
350
|
+
this.handle = acquireWorker();
|
|
351
|
+
activeWorkers.add(this.handle.worker);
|
|
352
|
+
await this.handle.ready;
|
|
353
|
+
if (this.finished || this.signal?.aborted)
|
|
354
|
+
return this.abort();
|
|
355
|
+
this.handle.worker.on("message", this.onMessage);
|
|
356
|
+
this.handle.worker.on("error", this.onError);
|
|
357
|
+
this.handle.worker.on("exit", this.onExit);
|
|
358
|
+
if (this.wall() >= this.timeoutMs)
|
|
359
|
+
return this.abort();
|
|
360
|
+
this.handle.worker.postMessage({ op: "run", runId: this.runId, code: this.code, timeoutMs: this.timeoutMs });
|
|
361
|
+
}
|
|
362
|
+
catch (cause) {
|
|
363
|
+
if (this.finished)
|
|
364
|
+
return;
|
|
365
|
+
this.fail("codemode worker unavailable: " + safeErrorMessage(cause));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
start() {
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
this.resolve = resolve;
|
|
371
|
+
this.timer = setTimeout(() => this.abort(), this.timeoutMs);
|
|
372
|
+
this.timer.unref?.();
|
|
373
|
+
this.memTimer = setInterval(() => {
|
|
374
|
+
if (process.memoryUsage().rss <= this.rssLimit)
|
|
375
|
+
return;
|
|
376
|
+
this.fail(this.memoryLimitError());
|
|
377
|
+
}, MEMORY_POLL_MS);
|
|
378
|
+
this.memTimer.unref?.();
|
|
379
|
+
this.signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
380
|
+
void this.boot();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
169
383
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
return !("error" in message)
|
|
191
|
-
|| message.error === undefined
|
|
192
|
-
|| (typeof message.error === "string" && message.error.length <= MAX_ERROR_CHARS);
|
|
384
|
+
function bindHostMethods(asgrep) {
|
|
385
|
+
const wrap = (fn) => (args, options) => fn(args, options);
|
|
386
|
+
return {
|
|
387
|
+
search: wrap(asgrep.search.bind(asgrep)),
|
|
388
|
+
find: wrap(asgrep.find.bind(asgrep)),
|
|
389
|
+
read: wrap(asgrep.read.bind(asgrep)),
|
|
390
|
+
edit: wrap(asgrep.edit.bind(asgrep)),
|
|
391
|
+
semantic: wrap(asgrep.semantic.bind(asgrep)),
|
|
392
|
+
chain: wrap(asgrep.chain.bind(asgrep)),
|
|
393
|
+
defs: wrap(asgrep.defs.bind(asgrep)),
|
|
394
|
+
callers: wrap(asgrep.callers.bind(asgrep)),
|
|
395
|
+
imports: wrap(asgrep.imports.bind(asgrep)),
|
|
396
|
+
indexStatus: (_args, options) => asgrep.indexStatus(options),
|
|
397
|
+
indexRepo: wrap(asgrep.indexRepo.bind(asgrep)),
|
|
398
|
+
doctor: (_args, options) => asgrep.doctor(options),
|
|
399
|
+
catalogSearch: wrap(asgrep.catalogSearch.bind(asgrep)),
|
|
400
|
+
catalogDescribe: wrap(asgrep.catalogDescribe.bind(asgrep)),
|
|
401
|
+
};
|
|
193
402
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
403
|
+
/** NAPI u64/i64 fields cross as BigInt; keep safe ints numeric, exact-string the rest. */
|
|
404
|
+
const jsonSafe = (_key, item) => typeof item === "bigint"
|
|
405
|
+
? item >= -9007199254740991n && item <= 9007199254740991n
|
|
406
|
+
? Number(item)
|
|
407
|
+
: item.toString()
|
|
408
|
+
: item;
|
|
409
|
+
/**
|
|
410
|
+
* Run model-generated JavaScript against the typed asgrep connector.
|
|
411
|
+
*
|
|
412
|
+
* Execution happens in a single-use worker_threads isolate: the guest gets a
|
|
413
|
+
* node:vm context inside the worker; asgrep/console are built there; the only
|
|
414
|
+
* host channel is a JSON postMessage bridge carrying runId envelopes. Timeout
|
|
415
|
+
* and abort call worker.terminate(), which is the only mechanism that actually
|
|
416
|
+
* stops a detached guest microtask or a runaway heap.
|
|
417
|
+
*/
|
|
418
|
+
export function runCodemode(rawCode, asgrep, options = {}) {
|
|
419
|
+
const admitted = admitRun(rawCode, options.timeoutMs, options.signal);
|
|
420
|
+
const wall0 = performance.now();
|
|
421
|
+
if ("error" in admitted) {
|
|
422
|
+
const out = {
|
|
423
|
+
ok: false, result: null, logs: [], error: admitted.error,
|
|
424
|
+
code: rawCode.slice(0, 200), wallMs: performance.now() - wall0,
|
|
425
|
+
};
|
|
426
|
+
const stats = options.stats?.();
|
|
427
|
+
if (stats)
|
|
428
|
+
out.stats = stats;
|
|
429
|
+
return Promise.resolve(out);
|
|
430
|
+
}
|
|
431
|
+
return new GuestRun(admitted.code, admitted.timeoutMs, bindHostMethods(asgrep), options.signal, options.stats, ++runSeq).start();
|
|
205
432
|
}
|
|
206
433
|
function safeErrorMessage(cause) {
|
|
207
434
|
try {
|
|
@@ -211,32 +438,3 @@ function safeErrorMessage(cause) {
|
|
|
211
438
|
return "codemode call failed";
|
|
212
439
|
}
|
|
213
440
|
}
|
|
214
|
-
function stringifyBounded(value, maxBytes, label) {
|
|
215
|
-
let remaining = maxBytes;
|
|
216
|
-
const payload = JSON.stringify(value, (key, item) => {
|
|
217
|
-
remaining -= Buffer.byteLength(key) + 8;
|
|
218
|
-
if (typeof item === "string")
|
|
219
|
-
remaining -= Buffer.byteLength(item);
|
|
220
|
-
if (remaining < 0)
|
|
221
|
-
throw new Error(`${label} exceeds ${maxBytes} bytes`);
|
|
222
|
-
return item;
|
|
223
|
-
});
|
|
224
|
-
if (payload === undefined || Buffer.byteLength(payload) > maxBytes) {
|
|
225
|
-
throw new Error(`${label} exceeds ${maxBytes} bytes`);
|
|
226
|
-
}
|
|
227
|
-
return payload;
|
|
228
|
-
}
|
|
229
|
-
function resultOk(result, logs, code, wall0, statsFn) {
|
|
230
|
-
const out = { ok: true, result, logs, code, wallMs: Date.now() - wall0 };
|
|
231
|
-
const stats = statsFn?.();
|
|
232
|
-
if (stats)
|
|
233
|
-
out.stats = stats;
|
|
234
|
-
return out;
|
|
235
|
-
}
|
|
236
|
-
function resultErr(error, logs, code, wall0, statsFn) {
|
|
237
|
-
const out = { ok: false, result: null, logs, error, code, wallMs: Date.now() - wall0 };
|
|
238
|
-
const stats = statsFn?.();
|
|
239
|
-
if (stats)
|
|
240
|
-
out.stats = stats;
|
|
241
|
-
return out;
|
|
242
|
-
}
|