@alisio/core 0.5.2 → 0.6.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/dist/analysis/janitor.d.ts +48 -0
- package/dist/analysis/janitor.js +170 -18
- package/dist/analysis/jobs.d.ts +11 -2
- package/dist/analysis/jobs.js +13 -0
- package/dist/analysis/oci.d.ts +11 -2
- package/dist/analysis/oci.js +20 -4
- package/dist/analysis/python/sources.js +2 -1
- package/dist/analysis/rerun.d.ts +1 -1
- package/dist/analysis/status.d.ts +3 -1
- package/dist/analysis/status.js +3 -0
- package/dist/application.d.ts +28 -2
- package/dist/application.js +189 -4
- package/dist/changelog/data.js +26 -0
- package/dist/config.d.ts +110 -2
- package/dist/config.js +188 -6
- package/dist/core/call-internals.d.ts +22 -0
- package/dist/core/call-internals.js +9 -0
- package/dist/core/contracts.d.ts +2 -2
- package/dist/core/runner.js +111 -38
- package/dist/forge/broker.d.ts +51 -0
- package/dist/forge/broker.js +244 -0
- package/dist/forge/budget.d.ts +27 -0
- package/dist/forge/budget.js +60 -0
- package/dist/forge/catalog-view.d.ts +24 -0
- package/dist/forge/catalog-view.js +43 -0
- package/dist/forge/errors.d.ts +26 -0
- package/dist/forge/errors.js +49 -0
- package/dist/forge/guides/javascript.d.ts +25 -0
- package/dist/forge/guides/javascript.js +90 -0
- package/dist/forge/guides/provider.d.ts +42 -0
- package/dist/forge/guides/provider.js +96 -0
- package/dist/forge/guides/python-environment.d.ts +15 -0
- package/dist/forge/guides/python-environment.js +81 -0
- package/dist/forge/guides/python.d.ts +38 -0
- package/dist/forge/guides/python.js +170 -0
- package/dist/forge/javascript/engine.d.ts +44 -0
- package/dist/forge/javascript/engine.js +229 -0
- package/dist/forge/javascript/hosts/factory.d.ts +25 -0
- package/dist/forge/javascript/hosts/factory.js +21 -0
- package/dist/forge/javascript/hosts/framing.d.ts +17 -0
- package/dist/forge/javascript/hosts/framing.js +49 -0
- package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
- package/dist/forge/javascript/hosts/isolate-main.js +63 -0
- package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/process-entry.js +3 -0
- package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
- package/dist/forge/javascript/hosts/process-host.js +160 -0
- package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/worker-entry.js +3 -0
- package/dist/forge/javascript/hosts/worker-host.d.ts +18 -0
- package/dist/forge/javascript/hosts/worker-host.js +101 -0
- package/dist/forge/javascript/prelude.d.ts +2 -0
- package/dist/forge/javascript/prelude.js +333 -0
- package/dist/forge/javascript/probe.d.ts +2 -0
- package/dist/forge/javascript/probe.js +53 -0
- package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
- package/dist/forge/javascript/runtime-manifest.js +14 -0
- package/dist/forge/javascript/vm-runner.d.ts +21 -0
- package/dist/forge/javascript/vm-runner.js +301 -0
- package/dist/forge/javascript/wasm-asset.d.ts +8 -0
- package/dist/forge/javascript/wasm-asset.js +8 -0
- package/dist/forge/javascript/wasm.d.ts +15 -0
- package/dist/forge/javascript/wasm.js +54 -0
- package/dist/forge/journal.d.ts +105 -0
- package/dist/forge/journal.js +174 -0
- package/dist/forge/kernel.d.ts +76 -0
- package/dist/forge/kernel.js +338 -0
- package/dist/forge/limits.d.ts +38 -0
- package/dist/forge/limits.js +73 -0
- package/dist/forge/nested.d.ts +96 -0
- package/dist/forge/nested.js +214 -0
- package/dist/forge/ports.d.ts +120 -0
- package/dist/forge/ports.js +1 -0
- package/dist/forge/progress.d.ts +24 -0
- package/dist/forge/progress.js +76 -0
- package/dist/forge/protocol.d.ts +110 -0
- package/dist/forge/protocol.js +171 -0
- package/dist/forge/python/backend-selector.d.ts +38 -0
- package/dist/forge/python/backend-selector.js +111 -0
- package/dist/forge/python/backend.d.ts +89 -0
- package/dist/forge/python/backend.js +1 -0
- package/dist/forge/python/engine.d.ts +84 -0
- package/dist/forge/python/engine.js +279 -0
- package/dist/forge/python/frame-parser.d.ts +24 -0
- package/dist/forge/python/frame-parser.js +107 -0
- package/dist/forge/python/frame-transport.d.ts +40 -0
- package/dist/forge/python/frame-transport.js +101 -0
- package/dist/forge/python/import-scan.d.ts +22 -0
- package/dist/forge/python/import-scan.js +133 -0
- package/dist/forge/python/managed-backend.d.ts +30 -0
- package/dist/forge/python/managed-backend.js +111 -0
- package/dist/forge/python/oci-backend.d.ts +27 -0
- package/dist/forge/python/oci-backend.js +84 -0
- package/dist/forge/python/pyodide-backend.d.ts +40 -0
- package/dist/forge/python/pyodide-backend.js +289 -0
- package/dist/forge/python/pyodide-install.d.ts +41 -0
- package/dist/forge/python/pyodide-install.js +167 -0
- package/dist/forge/python/pyodide-manifest.d.ts +42 -0
- package/dist/forge/python/pyodide-manifest.js +376 -0
- package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
- package/dist/forge/python/pyodide-runner-source.js +3 -0
- package/dist/forge/python/rss.d.ts +2 -0
- package/dist/forge/python/rss.js +33 -0
- package/dist/forge/python/scratch-copy.d.ts +6 -0
- package/dist/forge/python/scratch-copy.js +61 -0
- package/dist/forge/python/settle.d.ts +6 -0
- package/dist/forge/python/settle.js +21 -0
- package/dist/forge/python/status.d.ts +8 -0
- package/dist/forge/python/status.js +15 -0
- package/dist/forge/python/wasm-node.d.ts +59 -0
- package/dist/forge/python/wasm-node.js +220 -0
- package/dist/forge/python/wiring.d.ts +39 -0
- package/dist/forge/python/wiring.js +47 -0
- package/dist/forge/registry.d.ts +26 -0
- package/dist/forge/registry.js +83 -0
- package/dist/forge/result.d.ts +58 -0
- package/dist/forge/result.js +86 -0
- package/dist/forge/routing-rule.d.ts +19 -0
- package/dist/forge/routing-rule.js +31 -0
- package/dist/forge/runtime.d.ts +64 -0
- package/dist/forge/runtime.js +88 -0
- package/dist/forge/scratch-files.d.ts +36 -0
- package/dist/forge/scratch-files.js +94 -0
- package/dist/forge/scratch-provider.d.ts +41 -0
- package/dist/forge/scratch-provider.js +79 -0
- package/dist/forge/scratch-quota.d.ts +8 -0
- package/dist/forge/scratch-quota.js +25 -0
- package/dist/forge/selector.d.ts +35 -0
- package/dist/forge/selector.js +51 -0
- package/dist/forge/session-dir.d.ts +59 -0
- package/dist/forge/session-dir.js +239 -0
- package/dist/forge/session-path.d.ts +31 -0
- package/dist/forge/session-path.js +61 -0
- package/dist/forge/slots.d.ts +16 -0
- package/dist/forge/slots.js +47 -0
- package/dist/forge/state-delta.d.ts +54 -0
- package/dist/forge/state-delta.js +99 -0
- package/dist/forge/state-machine.d.ts +20 -0
- package/dist/forge/state-machine.js +40 -0
- package/dist/forge/state-store.d.ts +25 -0
- package/dist/forge/state-store.js +50 -0
- package/dist/forge/task-log.d.ts +9 -0
- package/dist/forge/task-log.js +42 -0
- package/dist/forge/verdict.d.ts +20 -0
- package/dist/forge/verdict.js +28 -0
- package/dist/forge-isolate.d.ts +6 -0
- package/dist/forge-isolate.js +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/resources/context.d.ts +2 -1
- package/dist/resources/context.js +2 -1
- package/dist/runtime/access.d.ts +34 -0
- package/dist/runtime/access.js +80 -0
- package/dist/runtime/process.d.ts +2 -1
- package/dist/runtime/process.js +5 -5
- package/dist/runtime/store.d.ts +11 -0
- package/dist/runtime/store.js +103 -0
- package/dist/tools/analysis.d.ts +25 -2
- package/dist/tools/analysis.js +315 -267
- package/dist/tools/execute.js +6 -7
- package/dist/tools/forge-guide.d.ts +6 -0
- package/dist/tools/forge-guide.js +60 -0
- package/dist/tools/js-run.d.ts +19 -0
- package/dist/tools/js-run.js +126 -0
- package/dist/tools/standard.d.ts +7 -1
- package/dist/tools/standard.js +44 -43
- package/package.json +7 -2
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guest side of the JavaScript engine (spec §6, D-26): runs INSIDE the isolate (a worker
|
|
3
|
+
* thread or a child process). It creates one QuickJS VM per execution, evaluates the prelude,
|
|
4
|
+
* bridges `tools.call`, `catalog.*`, `state.*`, `text`, `progress` to protocol messages and drives
|
|
5
|
+
* the VM's job queue until the root promise settles.
|
|
6
|
+
*
|
|
7
|
+
* The host never trusts anything produced here: every message is validated against the protocol,
|
|
8
|
+
* the call budget and the sizes again on the other side. This runner only has to be a faithful
|
|
9
|
+
* and well-behaved guest; if it is not, the host ends the execution.
|
|
10
|
+
*
|
|
11
|
+
* Erasable TypeScript only (D-33): the isolate entry loads this file under Node's type stripping.
|
|
12
|
+
*/
|
|
13
|
+
import { JSValueHandle, QuickJS } from "quickjs-wasi";
|
|
14
|
+
import { PRELUDE_HASH, PRELUDE_SOURCE } from "./prelude.js";
|
|
15
|
+
import { FORGE_PROTOCOL_VERSION } from "./runtime-manifest.js";
|
|
16
|
+
/** What a failed script reports: a limit only when the engine's own error type said so (L7). */
|
|
17
|
+
const classify = (info) => {
|
|
18
|
+
const message = info.message ?? "";
|
|
19
|
+
if (info.limit === "MEMORY_LIMIT" || info.limit === "STACK_LIMIT")
|
|
20
|
+
return { code: info.limit, message };
|
|
21
|
+
return { code: info.code ?? "SCRIPT_RUNTIME", message };
|
|
22
|
+
};
|
|
23
|
+
/** The VM itself failed outside the script (the exception comes from the engine, not from guest text). */
|
|
24
|
+
const classifyEngineFailure = (message) => {
|
|
25
|
+
if (/out of memory/i.test(message))
|
|
26
|
+
return "MEMORY_LIMIT";
|
|
27
|
+
if (/call stack size/i.test(message))
|
|
28
|
+
return "STACK_LIMIT";
|
|
29
|
+
return "SCRIPT_RUNTIME";
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Runs one execution to its terminal message (`complete` or `fatal`) and returns. The caller closes
|
|
33
|
+
* the transport (and exits the process) afterwards.
|
|
34
|
+
*/
|
|
35
|
+
export function runGuest(options) {
|
|
36
|
+
const { transport, module } = options;
|
|
37
|
+
const now = options.now ?? (() => performance.now());
|
|
38
|
+
return new Promise((finish) => {
|
|
39
|
+
let seq = 0;
|
|
40
|
+
let started = false;
|
|
41
|
+
let done = false;
|
|
42
|
+
const send = (type, payload) => {
|
|
43
|
+
if (done && type !== "complete" && type !== "fatal")
|
|
44
|
+
return;
|
|
45
|
+
transport.send({ protocol: FORGE_PROTOCOL_VERSION, type, seq: seq++, payload });
|
|
46
|
+
};
|
|
47
|
+
let vm;
|
|
48
|
+
let api;
|
|
49
|
+
let root;
|
|
50
|
+
let deadline = Number.POSITIVE_INFINITY;
|
|
51
|
+
let interrupted;
|
|
52
|
+
const calls = new Map();
|
|
53
|
+
const catalogs = new Map();
|
|
54
|
+
let nextCatalogId = 0;
|
|
55
|
+
/** Credits that arrive while the VM is still being created. */
|
|
56
|
+
let earlyCredits = 0;
|
|
57
|
+
/** A terminal condition the prelude asked for (a spent call budget). */
|
|
58
|
+
let requested;
|
|
59
|
+
const end = (type, payload) => {
|
|
60
|
+
if (done)
|
|
61
|
+
return;
|
|
62
|
+
send(type, payload);
|
|
63
|
+
done = true;
|
|
64
|
+
try {
|
|
65
|
+
vm?.dispose();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* The VM is going away with the isolate. */
|
|
69
|
+
}
|
|
70
|
+
finish();
|
|
71
|
+
};
|
|
72
|
+
const fatal = (code, message) => end("fatal", { code, message });
|
|
73
|
+
const interruptHandler = () => {
|
|
74
|
+
const flag = options.flag ? Atomics.load(options.flag, 0) : 0;
|
|
75
|
+
if (flag !== 0) {
|
|
76
|
+
interrupted = flag === 2 ? "timeout" : "cancelled";
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (now() >= deadline) {
|
|
80
|
+
interrupted = "timeout";
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
const bridge = (kind, data) => {
|
|
86
|
+
const machine = vm;
|
|
87
|
+
const body = JSON.parse(data);
|
|
88
|
+
switch (kind) {
|
|
89
|
+
case "call": {
|
|
90
|
+
const callId = Number(body.callId);
|
|
91
|
+
const deferred = machine.newPromise();
|
|
92
|
+
calls.set(callId, deferred);
|
|
93
|
+
send("call", { callId, tool: body.tool, input: JSON.parse(String(body.input)) });
|
|
94
|
+
return deferred.handle;
|
|
95
|
+
}
|
|
96
|
+
case "catalog": {
|
|
97
|
+
const requestId = nextCatalogId++;
|
|
98
|
+
const deferred = machine.newPromise();
|
|
99
|
+
catalogs.set(requestId, deferred);
|
|
100
|
+
send("catalog", {
|
|
101
|
+
requestId,
|
|
102
|
+
op: body.op,
|
|
103
|
+
query: body.query,
|
|
104
|
+
...(typeof body.limit === "number" ? { limit: body.limit } : {}),
|
|
105
|
+
});
|
|
106
|
+
return deferred.handle;
|
|
107
|
+
}
|
|
108
|
+
case "output":
|
|
109
|
+
send("output", body);
|
|
110
|
+
return undefined;
|
|
111
|
+
case "progress":
|
|
112
|
+
send("progress", {
|
|
113
|
+
message: body.message,
|
|
114
|
+
...(typeof body.completed === "number" ? { completed: body.completed } : {}),
|
|
115
|
+
...(typeof body.total === "number" ? { total: body.total } : {}),
|
|
116
|
+
});
|
|
117
|
+
return undefined;
|
|
118
|
+
case "state.set":
|
|
119
|
+
send("state.set", { key: body.key, value: JSON.parse(String(body.value)) });
|
|
120
|
+
return undefined;
|
|
121
|
+
case "state.delete":
|
|
122
|
+
send("state.delete", { key: body.key });
|
|
123
|
+
return undefined;
|
|
124
|
+
case "fatal":
|
|
125
|
+
// Ends the execution once the VM is back on the host stack (`pump` looks, never here).
|
|
126
|
+
requested ??= { code: String(body.code), message: String(body.message) };
|
|
127
|
+
return undefined;
|
|
128
|
+
default:
|
|
129
|
+
throw new Error(`unknown bridge request ${kind}`);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const applyGrant = (credits) => {
|
|
133
|
+
const machine = vm;
|
|
134
|
+
const count = machine.newNumber(credits);
|
|
135
|
+
machine
|
|
136
|
+
.callFunction(api.grant, machine.undefined, count)
|
|
137
|
+
.dispose();
|
|
138
|
+
count.dispose();
|
|
139
|
+
};
|
|
140
|
+
/** Runs queued promise jobs, then checks how the root promise stands. */
|
|
141
|
+
const pump = () => {
|
|
142
|
+
if (done || !vm || !root)
|
|
143
|
+
return;
|
|
144
|
+
try {
|
|
145
|
+
vm.executePendingJobs();
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
149
|
+
if (interrupted)
|
|
150
|
+
return fatal(interrupted === "timeout" ? "TIMEOUT" : "CANCELLED", "interrupted");
|
|
151
|
+
if (requested)
|
|
152
|
+
return fatal(requested.code, requested.message);
|
|
153
|
+
return fatal(classifyEngineFailure(message), message);
|
|
154
|
+
}
|
|
155
|
+
if (done)
|
|
156
|
+
return;
|
|
157
|
+
if (requested)
|
|
158
|
+
return fatal(requested.code, requested.message);
|
|
159
|
+
const exports = vm._getExports();
|
|
160
|
+
const state = exports.qjs_promise_state(root.ptr);
|
|
161
|
+
if (state === 0) {
|
|
162
|
+
if (calls.size + catalogs.size > 0)
|
|
163
|
+
return;
|
|
164
|
+
const waiting = vm.callFunction(api?.waiting, vm.undefined);
|
|
165
|
+
const queued = Number(vm.dump(waiting));
|
|
166
|
+
waiting.dispose();
|
|
167
|
+
if (queued === 0)
|
|
168
|
+
fatal("UNRESOLVABLE_PROMISE", "the script is waiting on a promise that nothing can settle");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const resultHandle = new JSValueHandle(vm, exports.qjs_promise_result(root.ptr));
|
|
172
|
+
const text = vm.dump(resultHandle);
|
|
173
|
+
resultHandle.dispose();
|
|
174
|
+
if (state === 2) {
|
|
175
|
+
// `run` never rejects; a rejection here means the VM itself failed.
|
|
176
|
+
return fatal("SCRIPT_RUNTIME", `script failed: ${String(text)}`);
|
|
177
|
+
}
|
|
178
|
+
const info = JSON.parse(String(text));
|
|
179
|
+
if (info.ok)
|
|
180
|
+
end("complete", info.value === undefined ? {} : { value: info.value });
|
|
181
|
+
else {
|
|
182
|
+
const { code, message } = classify(info);
|
|
183
|
+
fatal(code, message);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const start = async (payload) => {
|
|
187
|
+
try {
|
|
188
|
+
deadline = now() + payload.limits.timeoutMs;
|
|
189
|
+
vm = await QuickJS.create({
|
|
190
|
+
wasm: module,
|
|
191
|
+
memoryLimit: payload.limits.memoryBytes,
|
|
192
|
+
maxStackSize: payload.limits.stackBytes,
|
|
193
|
+
interruptHandler,
|
|
194
|
+
moduleLoader: {
|
|
195
|
+
load: () => {
|
|
196
|
+
throw new Error("modules are not available");
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const machine = vm;
|
|
201
|
+
const bridgeFn = machine.newFunction("__forge_bridge", (kind, data) => {
|
|
202
|
+
const result = bridge(String(machine.dump(kind)), String(machine.dump(data)));
|
|
203
|
+
return result ?? machine.undefined;
|
|
204
|
+
});
|
|
205
|
+
const preludeFn = machine.evalCode(PRELUDE_SOURCE, "prelude.js");
|
|
206
|
+
const exported = machine.callFunction(preludeFn, machine.undefined, bridgeFn);
|
|
207
|
+
api = {
|
|
208
|
+
init: exported.getProp("init"),
|
|
209
|
+
run: exported.getProp("run"),
|
|
210
|
+
grant: exported.getProp("grant"),
|
|
211
|
+
waiting: exported.getProp("waiting"),
|
|
212
|
+
};
|
|
213
|
+
const limitsJson = machine.newString(JSON.stringify(payload.limits));
|
|
214
|
+
const stateJson = machine.newString(JSON.stringify(payload.state));
|
|
215
|
+
machine.callFunction(api.init, machine.undefined, limitsJson, stateJson).dispose();
|
|
216
|
+
limitsJson.dispose();
|
|
217
|
+
stateJson.dispose();
|
|
218
|
+
if (earlyCredits > 0)
|
|
219
|
+
applyGrant(earlyCredits);
|
|
220
|
+
earlyCredits = 0;
|
|
221
|
+
let body;
|
|
222
|
+
try {
|
|
223
|
+
body = machine.evalCode(`(async function () {\n${payload.source}\n})`, "js_run.js");
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
+
return fatal("SCRIPT_SYNTAX", message);
|
|
228
|
+
}
|
|
229
|
+
root = machine.callFunction(api.run, machine.undefined, body);
|
|
230
|
+
pump();
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (done)
|
|
234
|
+
return;
|
|
235
|
+
if (interrupted)
|
|
236
|
+
return fatal(interrupted === "timeout" ? "TIMEOUT" : "CANCELLED", "interrupted");
|
|
237
|
+
fatal("ISOLATE_FAILURE", error instanceof Error ? error.message : String(error));
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
transport.onMessage((raw) => {
|
|
241
|
+
if (done)
|
|
242
|
+
return;
|
|
243
|
+
const message = raw;
|
|
244
|
+
try {
|
|
245
|
+
switch (message.type) {
|
|
246
|
+
case "start":
|
|
247
|
+
if (started)
|
|
248
|
+
return;
|
|
249
|
+
started = true;
|
|
250
|
+
void start(message.payload);
|
|
251
|
+
return;
|
|
252
|
+
case "result": {
|
|
253
|
+
const payload = message.payload;
|
|
254
|
+
const deferred = calls.get(payload.callId);
|
|
255
|
+
if (!deferred || !vm)
|
|
256
|
+
return;
|
|
257
|
+
calls.delete(payload.callId);
|
|
258
|
+
const value = vm.newString(JSON.stringify(payload.ok ? payload.value : payload.error));
|
|
259
|
+
if (payload.ok)
|
|
260
|
+
deferred.resolve(value);
|
|
261
|
+
else
|
|
262
|
+
deferred.reject(value);
|
|
263
|
+
value.dispose();
|
|
264
|
+
pump();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case "catalog.result": {
|
|
268
|
+
const payload = message.payload;
|
|
269
|
+
const deferred = catalogs.get(payload.requestId);
|
|
270
|
+
if (!deferred || !vm)
|
|
271
|
+
return;
|
|
272
|
+
catalogs.delete(payload.requestId);
|
|
273
|
+
const value = vm.newString(JSON.stringify(payload.entries));
|
|
274
|
+
deferred.resolve(value);
|
|
275
|
+
value.dispose();
|
|
276
|
+
pump();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
case "grant": {
|
|
280
|
+
const payload = message.payload;
|
|
281
|
+
if (!vm || !api) {
|
|
282
|
+
earlyCredits += payload.calls;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
applyGrant(payload.calls);
|
|
286
|
+
pump();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
default:
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (interrupted)
|
|
295
|
+
return fatal(interrupted === "timeout" ? "TIMEOUT" : "CANCELLED", "interrupted");
|
|
296
|
+
fatal("ISOLATE_FAILURE", error instanceof Error ? error.message : String(error));
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
send("ready", { engine: "javascript-quickjs", preludeHash: PRELUDE_HASH });
|
|
300
|
+
});
|
|
301
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun-only: the WASM as an embedded asset. Imported dynamically, and only when `process.versions.bun`
|
|
3
|
+
* is set (`wasm.ts`), so Node never touches it. In a compiled binary the default export is a path
|
|
4
|
+
* into the binary's virtual file system that `node:fs` can read; running from sources it is the
|
|
5
|
+
* real file.
|
|
6
|
+
*/
|
|
7
|
+
import path from "quickjs-wasi/quickjs.wasm";
|
|
8
|
+
export default path;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun-only: the WASM as an embedded asset. Imported dynamically, and only when `process.versions.bun`
|
|
3
|
+
* is set (`wasm.ts`), so Node never touches it. In a compiled binary the default export is a path
|
|
4
|
+
* into the binary's virtual file system that `node:fs` can read; running from sources it is the
|
|
5
|
+
* real file.
|
|
6
|
+
*/
|
|
7
|
+
import path from "quickjs-wasi/quickjs.wasm" with { type: "file" };
|
|
8
|
+
export default path;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const sha256Hex: (data: Uint8Array | string) => string;
|
|
2
|
+
/** Why `bytes` is not the pinned WASM, or undefined when it is. */
|
|
3
|
+
export declare function wasmProblem(bytes: Uint8Array): string | undefined;
|
|
4
|
+
/** The path of the WASM: the Bun asset on Bun, the package file on Node. */
|
|
5
|
+
export declare function resolveWasmPath(): Promise<string>;
|
|
6
|
+
export type WasmBytesLoader = () => Promise<Uint8Array>;
|
|
7
|
+
/**
|
|
8
|
+
* Reads, verifies and compiles. Throws an `Error` whose message says why when the bytes are not the
|
|
9
|
+
* pinned build. Not cached: see `loadQuickJsModule`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function compileVerifiedModule(loader?: WasmBytesLoader): Promise<WebAssembly.Module>;
|
|
12
|
+
/** The process-wide compiled module. A failure is cached too: the engine stays not ready. */
|
|
13
|
+
export declare function loadQuickJsModule(): Promise<WebAssembly.Module>;
|
|
14
|
+
/** Test seam: forgets the cached module. */
|
|
15
|
+
export declare function resetQuickJsModuleCache(): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads, verifies and compiles the QuickJS WASM (spec D3, D17, D-22).
|
|
3
|
+
*
|
|
4
|
+
* The bytes come from the Bun asset (guarded dynamic import) or, on Node, from
|
|
5
|
+
* `quickjs-wasi/quickjs.wasm`. Their SHA-256 is compared with the manifest BEFORE compiling: a
|
|
6
|
+
* mismatch means the engine is not ready. The compiled `WebAssembly.Module` is cached per process
|
|
7
|
+
* and reused by every execution (the memory and globals of a VM never are).
|
|
8
|
+
*
|
|
9
|
+
* Erasable TypeScript only (D-33): the isolate entry imports this file under Node's type stripping.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { QUICKJS_WASM_BYTES, QUICKJS_WASM_SHA256 } from "./runtime-manifest.js";
|
|
15
|
+
export const sha256Hex = (data) => createHash("sha256").update(data).digest("hex");
|
|
16
|
+
/** Why `bytes` is not the pinned WASM, or undefined when it is. */
|
|
17
|
+
export function wasmProblem(bytes) {
|
|
18
|
+
if (bytes.byteLength !== QUICKJS_WASM_BYTES)
|
|
19
|
+
return `quickjs.wasm has ${bytes.byteLength} bytes, expected ${QUICKJS_WASM_BYTES}`;
|
|
20
|
+
const actual = sha256Hex(bytes);
|
|
21
|
+
if (actual !== QUICKJS_WASM_SHA256)
|
|
22
|
+
return `quickjs.wasm hash ${actual} does not match the pinned ${QUICKJS_WASM_SHA256}`;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
/** The path of the WASM: the Bun asset on Bun, the package file on Node. */
|
|
26
|
+
export async function resolveWasmPath() {
|
|
27
|
+
if (process.versions.bun) {
|
|
28
|
+
const asset = await import("./wasm-asset.js");
|
|
29
|
+
return asset.default;
|
|
30
|
+
}
|
|
31
|
+
return createRequire(import.meta.url).resolve("quickjs-wasi/quickjs.wasm");
|
|
32
|
+
}
|
|
33
|
+
const defaultLoader = async () => readFile(await resolveWasmPath());
|
|
34
|
+
/**
|
|
35
|
+
* Reads, verifies and compiles. Throws an `Error` whose message says why when the bytes are not the
|
|
36
|
+
* pinned build. Not cached: see `loadQuickJsModule`.
|
|
37
|
+
*/
|
|
38
|
+
export async function compileVerifiedModule(loader = defaultLoader) {
|
|
39
|
+
const bytes = await loader();
|
|
40
|
+
const problem = wasmProblem(bytes);
|
|
41
|
+
if (problem)
|
|
42
|
+
throw new Error(problem);
|
|
43
|
+
return WebAssembly.compile(bytes);
|
|
44
|
+
}
|
|
45
|
+
let cached;
|
|
46
|
+
/** The process-wide compiled module. A failure is cached too: the engine stays not ready. */
|
|
47
|
+
export function loadQuickJsModule() {
|
|
48
|
+
cached ??= compileVerifiedModule();
|
|
49
|
+
return cached;
|
|
50
|
+
}
|
|
51
|
+
/** Test seam: forgets the cached module. */
|
|
52
|
+
export function resetQuickJsModuleCache() {
|
|
53
|
+
cached = undefined;
|
|
54
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Forja execution journal (spec §9.1, §9.3) over the shared SQLite store.
|
|
3
|
+
*
|
|
4
|
+
* One `forge_executions` row per execution whose status follows the Phase 1 state machine, one
|
|
5
|
+
* `forge_calls` row per nested call, and a restart reconciliation for rows a dead process left
|
|
6
|
+
* non-terminal. Only hashes of inputs are stored, never inputs or results. Every status change is
|
|
7
|
+
* a compare-and-set on the previous status, so a terminal state is reached exactly once even with
|
|
8
|
+
* several processes sharing the database.
|
|
9
|
+
*/
|
|
10
|
+
import type { EngineId, IsolationLevel, SqlDatabase } from "@alisio/sdk";
|
|
11
|
+
import type { ForgeErrorCode } from "./errors.ts";
|
|
12
|
+
import { type ExecutionState } from "./state-machine.ts";
|
|
13
|
+
export type TerminalExecutionState = "succeeded" | "failed" | "cancelled" | "timed_out";
|
|
14
|
+
/** Effect class of a nested call, as the gate classifies it. */
|
|
15
|
+
export type CallEffect = "read" | "write" | "process" | "external" | "internal";
|
|
16
|
+
export type CallStatus = "started" | "succeeded" | "failed";
|
|
17
|
+
export interface OpenExecutionInput {
|
|
18
|
+
id: string;
|
|
19
|
+
rootSessionId: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
toolCallId: string;
|
|
22
|
+
engine: EngineId;
|
|
23
|
+
isolation: IsolationLevel;
|
|
24
|
+
selectionReason: string;
|
|
25
|
+
codeSha256: string;
|
|
26
|
+
analysisExecutionId?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface ExecutionRecord extends Omit<OpenExecutionInput, "analysisExecutionId"> {
|
|
29
|
+
status: ExecutionState;
|
|
30
|
+
startedAt: number;
|
|
31
|
+
endedAt?: number;
|
|
32
|
+
metrics?: Record<string, unknown>;
|
|
33
|
+
errorCode?: ForgeErrorCode;
|
|
34
|
+
terminationUnconfirmed: boolean;
|
|
35
|
+
analysisExecutionId?: string;
|
|
36
|
+
ownerPid?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface CloseOutcome {
|
|
39
|
+
status: TerminalExecutionState;
|
|
40
|
+
errorCode?: ForgeErrorCode;
|
|
41
|
+
metrics?: Record<string, unknown>;
|
|
42
|
+
/** The host could not verify that the guest stopped (D-21). */
|
|
43
|
+
terminationUnconfirmed?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface StartCallInput {
|
|
46
|
+
executionId: string;
|
|
47
|
+
/** 1-based position of the call within its execution. */
|
|
48
|
+
n: number;
|
|
49
|
+
toolName: string;
|
|
50
|
+
effect: CallEffect;
|
|
51
|
+
inputSha256: string;
|
|
52
|
+
/** Only kept for the `write` effect (D-31). */
|
|
53
|
+
paths?: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface CallRecord {
|
|
56
|
+
executionId: string;
|
|
57
|
+
callId: string;
|
|
58
|
+
toolName: string;
|
|
59
|
+
effect: CallEffect;
|
|
60
|
+
status: CallStatus;
|
|
61
|
+
durationMs?: number;
|
|
62
|
+
inputSha256: string;
|
|
63
|
+
errorCode?: ForgeErrorCode;
|
|
64
|
+
paths?: string[];
|
|
65
|
+
}
|
|
66
|
+
export interface EndCallOutcome {
|
|
67
|
+
status: "succeeded" | "failed";
|
|
68
|
+
durationMs?: number;
|
|
69
|
+
errorCode?: ForgeErrorCode;
|
|
70
|
+
}
|
|
71
|
+
/** The id of the n-th nested call of a tool call (D-24). It never enters `tool_calls`. */
|
|
72
|
+
export declare const forgeCallId: (toolCallId: string, n: number) => string;
|
|
73
|
+
export declare class SqliteExecutionJournal {
|
|
74
|
+
#private;
|
|
75
|
+
constructor(db: SqlDatabase, options?: {
|
|
76
|
+
now?: () => number;
|
|
77
|
+
ownerPid?: number;
|
|
78
|
+
});
|
|
79
|
+
/** Records a new execution in `validating`. */
|
|
80
|
+
open(input: OpenExecutionInput): ExecutionRecord;
|
|
81
|
+
/** Links the execution to the `analysis_executions` row its engine created while preparing. */
|
|
82
|
+
linkAnalysisExecution(id: string, analysisExecutionId: string): void;
|
|
83
|
+
get(id: string): ExecutionRecord | undefined;
|
|
84
|
+
listByRoot(rootSessionId: string): ExecutionRecord[];
|
|
85
|
+
/** Moves to a non-terminal state the diagram allows. Terminal states go through `close()`. */
|
|
86
|
+
transition(id: string, to: ExecutionState): void;
|
|
87
|
+
/** Reaches a terminal state, once. A second close throws and changes nothing. */
|
|
88
|
+
close(id: string, outcome: CloseOutcome): void;
|
|
89
|
+
/** Records the start of a nested call and returns its id. */
|
|
90
|
+
startCall(input: StartCallInput): string;
|
|
91
|
+
/** Ends a call that is still `started`; false when it already ended (or does not exist). */
|
|
92
|
+
endCall(executionId: string, callId: string, outcome: EndCallOutcome): boolean;
|
|
93
|
+
calls(executionId: string): CallRecord[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Restart reconciliation (§9.3, FJ-AC-25): every non-terminal execution whose owner process is
|
|
97
|
+
* gone (or unknown) becomes `failed` with `ISOLATE_FAILURE` and reason `interrupted`. Nothing is
|
|
98
|
+
* relaunched. Executions of another live process are left alone. Returns the rows reconciled.
|
|
99
|
+
*
|
|
100
|
+
* Call it once when the application opens the store (Phase 3 wires it next to the runner).
|
|
101
|
+
*/
|
|
102
|
+
export declare function reconcileInterruptedExecutions(db: SqlDatabase, options?: {
|
|
103
|
+
now?: () => number;
|
|
104
|
+
isAlive?: (pid: number) => boolean;
|
|
105
|
+
}): number;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { isProcessAlive } from "../runtime/process.js";
|
|
2
|
+
import { canTransition, InvalidTransitionError } from "./state-machine.js";
|
|
3
|
+
const TERMINAL = ["succeeded", "failed", "cancelled", "timed_out"];
|
|
4
|
+
const isTerminal = (state) => TERMINAL.includes(state);
|
|
5
|
+
/** The id of the n-th nested call of a tool call (D-24). It never enters `tool_calls`. */
|
|
6
|
+
export const forgeCallId = (toolCallId, n) => `${toolCallId}:forge:${n}`;
|
|
7
|
+
function toRecord(row) {
|
|
8
|
+
return {
|
|
9
|
+
id: String(row.id),
|
|
10
|
+
rootSessionId: String(row.root_session_id),
|
|
11
|
+
sessionId: String(row.session_id),
|
|
12
|
+
toolCallId: String(row.tool_call_id),
|
|
13
|
+
engine: row.engine,
|
|
14
|
+
isolation: row.isolation,
|
|
15
|
+
selectionReason: String(row.selection_reason),
|
|
16
|
+
status: row.status,
|
|
17
|
+
codeSha256: String(row.code_sha256),
|
|
18
|
+
startedAt: Number(row.started_at),
|
|
19
|
+
...(row.ended_at != null ? { endedAt: Number(row.ended_at) } : {}),
|
|
20
|
+
...(row.metrics_json != null
|
|
21
|
+
? { metrics: JSON.parse(String(row.metrics_json)) }
|
|
22
|
+
: {}),
|
|
23
|
+
...(row.error_code != null ? { errorCode: row.error_code } : {}),
|
|
24
|
+
terminationUnconfirmed: Number(row.termination_unconfirmed) === 1,
|
|
25
|
+
...(row.analysis_execution_id != null
|
|
26
|
+
? { analysisExecutionId: String(row.analysis_execution_id) }
|
|
27
|
+
: {}),
|
|
28
|
+
...(row.owner_pid != null ? { ownerPid: Number(row.owner_pid) } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function toCall(row) {
|
|
32
|
+
return {
|
|
33
|
+
executionId: String(row.execution_id),
|
|
34
|
+
callId: String(row.call_id),
|
|
35
|
+
toolName: String(row.tool_name),
|
|
36
|
+
effect: row.effect,
|
|
37
|
+
status: row.status,
|
|
38
|
+
...(row.duration_ms != null ? { durationMs: Number(row.duration_ms) } : {}),
|
|
39
|
+
inputSha256: String(row.input_sha256),
|
|
40
|
+
...(row.error_code != null ? { errorCode: row.error_code } : {}),
|
|
41
|
+
...(row.paths_json != null ? { paths: JSON.parse(String(row.paths_json)) } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export class SqliteExecutionJournal {
|
|
45
|
+
#db;
|
|
46
|
+
#now;
|
|
47
|
+
#ownerPid;
|
|
48
|
+
constructor(db, options = {}) {
|
|
49
|
+
this.#db = db;
|
|
50
|
+
this.#now = options.now ?? Date.now;
|
|
51
|
+
this.#ownerPid = options.ownerPid ?? process.pid;
|
|
52
|
+
}
|
|
53
|
+
/** Records a new execution in `validating`. */
|
|
54
|
+
open(input) {
|
|
55
|
+
this.#db
|
|
56
|
+
.prepare(`INSERT INTO forge_executions(id,root_session_id,session_id,tool_call_id,engine,isolation,
|
|
57
|
+
selection_reason,status,code_sha256,started_at,analysis_execution_id,owner_pid)
|
|
58
|
+
VALUES(?,?,?,?,?,?,?,'validating',?,?,?,?)`)
|
|
59
|
+
.run(input.id, input.rootSessionId, input.sessionId, input.toolCallId, input.engine, input.isolation, input.selectionReason, input.codeSha256, this.#now(), input.analysisExecutionId ?? null, this.#ownerPid);
|
|
60
|
+
return this.#required(input.id);
|
|
61
|
+
}
|
|
62
|
+
/** Links the execution to the `analysis_executions` row its engine created while preparing. */
|
|
63
|
+
linkAnalysisExecution(id, analysisExecutionId) {
|
|
64
|
+
const result = this.#db
|
|
65
|
+
.prepare("UPDATE forge_executions SET analysis_execution_id=? WHERE id=?")
|
|
66
|
+
.run(analysisExecutionId, id);
|
|
67
|
+
if (Number(result.changes) === 0)
|
|
68
|
+
throw new Error(`Unknown Forja execution: ${id}`);
|
|
69
|
+
}
|
|
70
|
+
get(id) {
|
|
71
|
+
const row = this.#db.prepare("SELECT * FROM forge_executions WHERE id=?").get(id);
|
|
72
|
+
return row ? toRecord(row) : undefined;
|
|
73
|
+
}
|
|
74
|
+
listByRoot(rootSessionId) {
|
|
75
|
+
return this.#db
|
|
76
|
+
.prepare("SELECT * FROM forge_executions WHERE root_session_id=? ORDER BY started_at, id")
|
|
77
|
+
.all(rootSessionId).map(toRecord);
|
|
78
|
+
}
|
|
79
|
+
/** Moves to a non-terminal state the diagram allows. Terminal states go through `close()`. */
|
|
80
|
+
transition(id, to) {
|
|
81
|
+
if (isTerminal(to))
|
|
82
|
+
throw new Error(`Terminal state ${to} is reached through close(), not transition()`);
|
|
83
|
+
this.#advance(id, to, {});
|
|
84
|
+
}
|
|
85
|
+
/** Reaches a terminal state, once. A second close throws and changes nothing. */
|
|
86
|
+
close(id, outcome) {
|
|
87
|
+
if (!isTerminal(outcome.status))
|
|
88
|
+
throw new Error(`close() needs a terminal state, got ${String(outcome.status)}`);
|
|
89
|
+
this.#advance(id, outcome.status, {
|
|
90
|
+
endedAt: this.#now(),
|
|
91
|
+
errorCode: outcome.errorCode ?? null,
|
|
92
|
+
metrics: outcome.metrics ? JSON.stringify(outcome.metrics) : null,
|
|
93
|
+
unconfirmed: outcome.terminationUnconfirmed ? 1 : 0,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
#advance(id, to, terminal) {
|
|
97
|
+
this.#db.transaction(() => {
|
|
98
|
+
const row = this.#db.prepare("SELECT status FROM forge_executions WHERE id=?").get(id);
|
|
99
|
+
if (!row)
|
|
100
|
+
throw new Error(`Unknown Forja execution: ${id}`);
|
|
101
|
+
if (!canTransition(row.status, to))
|
|
102
|
+
throw new InvalidTransitionError(row.status, to);
|
|
103
|
+
if (terminal.endedAt === undefined) {
|
|
104
|
+
this.#db
|
|
105
|
+
.prepare("UPDATE forge_executions SET status=? WHERE id=? AND status=?")
|
|
106
|
+
.run(to, id, row.status);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.#db
|
|
110
|
+
.prepare(`UPDATE forge_executions SET status=?, ended_at=?, error_code=?, metrics_json=?,
|
|
111
|
+
termination_unconfirmed=? WHERE id=? AND status=?`)
|
|
112
|
+
.run(to, terminal.endedAt, terminal.errorCode ?? null, terminal.metrics ?? null, terminal.unconfirmed ?? 0, id, row.status);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Records the start of a nested call and returns its id. */
|
|
116
|
+
startCall(input) {
|
|
117
|
+
const execution = this.#required(input.executionId);
|
|
118
|
+
const callId = forgeCallId(execution.toolCallId, input.n);
|
|
119
|
+
this.#db
|
|
120
|
+
.prepare(`INSERT INTO forge_calls(execution_id,call_id,tool_name,effect,status,input_sha256,paths_json)
|
|
121
|
+
VALUES(?,?,?,?,'started',?,?)`)
|
|
122
|
+
.run(input.executionId, callId, input.toolName, input.effect, input.inputSha256, input.effect === "write" && input.paths ? JSON.stringify(input.paths) : null);
|
|
123
|
+
return callId;
|
|
124
|
+
}
|
|
125
|
+
/** Ends a call that is still `started`; false when it already ended (or does not exist). */
|
|
126
|
+
endCall(executionId, callId, outcome) {
|
|
127
|
+
const result = this.#db
|
|
128
|
+
.prepare(`UPDATE forge_calls SET status=?, duration_ms=?, error_code=?
|
|
129
|
+
WHERE execution_id=? AND call_id=? AND status='started'`)
|
|
130
|
+
.run(outcome.status, outcome.durationMs ?? null, outcome.errorCode ?? null, executionId, callId);
|
|
131
|
+
return Number(result.changes) > 0;
|
|
132
|
+
}
|
|
133
|
+
calls(executionId) {
|
|
134
|
+
const rows = this.#db
|
|
135
|
+
.prepare("SELECT * FROM forge_calls WHERE execution_id=?")
|
|
136
|
+
.all(executionId);
|
|
137
|
+
const position = (callId) => Number(callId.slice(callId.lastIndexOf(":") + 1));
|
|
138
|
+
return rows.map(toCall).sort((a, b) => position(a.callId) - position(b.callId));
|
|
139
|
+
}
|
|
140
|
+
#required(id) {
|
|
141
|
+
const record = this.get(id);
|
|
142
|
+
if (!record)
|
|
143
|
+
throw new Error(`Unknown Forja execution: ${id}`);
|
|
144
|
+
return record;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Restart reconciliation (§9.3, FJ-AC-25): every non-terminal execution whose owner process is
|
|
149
|
+
* gone (or unknown) becomes `failed` with `ISOLATE_FAILURE` and reason `interrupted`. Nothing is
|
|
150
|
+
* relaunched. Executions of another live process are left alone. Returns the rows reconciled.
|
|
151
|
+
*
|
|
152
|
+
* Call it once when the application opens the store (Phase 3 wires it next to the runner).
|
|
153
|
+
*/
|
|
154
|
+
export function reconcileInterruptedExecutions(db, options = {}) {
|
|
155
|
+
const now = (options.now ?? Date.now)();
|
|
156
|
+
const isAlive = options.isAlive ?? isProcessAlive;
|
|
157
|
+
return db.transaction(() => {
|
|
158
|
+
const rows = db
|
|
159
|
+
.prepare(`SELECT id, status, owner_pid FROM forge_executions
|
|
160
|
+
WHERE status NOT IN ('succeeded','failed','cancelled','timed_out')`)
|
|
161
|
+
.all();
|
|
162
|
+
const update = db.prepare(`UPDATE forge_executions SET status='failed', error_code='ISOLATE_FAILURE', ended_at=?,
|
|
163
|
+
metrics_json=? WHERE id=? AND status=?`);
|
|
164
|
+
let reconciled = 0;
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
if (row.owner_pid != null && isAlive(row.owner_pid))
|
|
167
|
+
continue;
|
|
168
|
+
const result = update.run(now, JSON.stringify({ reason: "interrupted" }), row.id, row.status);
|
|
169
|
+
if (Number(result.changes) > 0)
|
|
170
|
+
reconciled++;
|
|
171
|
+
}
|
|
172
|
+
return reconciled;
|
|
173
|
+
});
|
|
174
|
+
}
|