@h-ai/runner 0.0.0-stage → 1.0.0-beta.25

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/index.js ADDED
@@ -0,0 +1,1982 @@
1
+ import { z } from 'zod';
2
+ import { Buffer } from 'buffer';
3
+ import { randomUUID, createHash } from 'crypto';
4
+ import { core, ok, err } from '@h-ai/core';
5
+ import { execFile, spawn } from 'child_process';
6
+ import { once } from 'events';
7
+ import { lstat, chmod, rm, readFile, readlink, stat, realpath, mkdtemp, writeFile, mkdir } from 'fs/promises';
8
+ import { tmpdir } from 'os';
9
+ import { join, posix, basename } from 'path';
10
+ import { promisify } from 'util';
11
+ import process from 'process';
12
+ import { reldb } from '@h-ai/reldb';
13
+
14
+ // src/runner-config.ts
15
+ var id = z.string().min(1).max(100).regex(/^[\w.-]+$/u);
16
+ var timeout = z.number().int().min(1).max(864e5);
17
+ var resources = z.strictObject({
18
+ cpu: z.number().finite().positive().max(1024),
19
+ memoryMiB: z.number().int().min(16).max(2097152),
20
+ pids: z.number().int().min(1).max(1048576),
21
+ scratchMiB: z.number().int().min(3).max(2097152)
22
+ });
23
+ var environment = z.strictObject({
24
+ id,
25
+ image: z.string().max(255).regex(/^(?:[\w.:/-]+@)?sha256:[a-f\d]{64}$/u),
26
+ backend: z.literal("container").default("container"),
27
+ architecture: z.enum(["x64", "arm64"]).optional(),
28
+ capabilities: z.array(id).max(64).default([]),
29
+ resources,
30
+ networkPolicy: z.enum(["offline", "internal"]).default("offline"),
31
+ network: id.optional()
32
+ }).refine((value) => value.networkPolicy === "offline" ? !value.network : !!value.network && ![
33
+ "host",
34
+ "bridge",
35
+ "none",
36
+ "default",
37
+ "podman"
38
+ ].includes(value.network));
39
+ var RunnerConfigSchema = z.strictObject({
40
+ nodeId: id,
41
+ executor: z.boolean().default(true),
42
+ engine: z.enum(["podman", "docker"]).default("podman"),
43
+ capacity: resources.default({
44
+ cpu: 4,
45
+ memoryMiB: 4096,
46
+ pids: 512,
47
+ scratchMiB: 2048
48
+ }),
49
+ /** 每个执行节点独立预留给工具作业的资源;宿主总预算须同时覆盖 capacity 和本项。 */
50
+ toolCapacity: resources.default({
51
+ cpu: 1,
52
+ memoryMiB: 1024,
53
+ pids: 128,
54
+ scratchMiB: 512
55
+ }),
56
+ /** 每个执行节点的工具作业总并发,包括等待清理及未核验的作业,默认 4。 */
57
+ toolMaxRunning: z.number().int().min(1).max(1e3).default(4),
58
+ leaseMs: z.number().int().min(1e3).max(12e4).default(1e4),
59
+ pollMs: z.number().int().min(10).max(1e3).default(50),
60
+ maxRunning: z.number().int().min(1).max(1e3).default(8),
61
+ maxRunningPerScope: z.number().int().min(1).max(1e3).default(4),
62
+ maxControlRunning: z.number().int().min(1).max(1e3).default(8),
63
+ maxQueued: z.number().int().min(1).max(1e5).default(5e3),
64
+ maxEvents: z.number().int().min(1).max(1e5).default(1e4),
65
+ /** 已结算任务、事件和幂等键的保留期限。未核验的 unknown 不受该期限影响。 */
66
+ retentionMs: z.number().int().min(36e5).max(365 * 864e5).default(7 * 864e5),
67
+ closeTimeoutMs: z.number().int().min(100).max(12e4).default(2e4),
68
+ environments: z.array(environment).max(1e3).default([])
69
+ }).refine((value) => value.pollMs * 3 < value.leaseMs && new Set(value.environments.map((entry) => entry.id)).size === value.environments.length);
70
+ var json = z.json();
71
+ var submitSchema = z.strictObject({
72
+ scope: z.strictObject({
73
+ tenantId: z.string().min(1).max(200),
74
+ userId: z.string().min(1).max(200)
75
+ }),
76
+ kind: id,
77
+ input: json,
78
+ idempotencyKey: z.string().min(1).max(200).optional(),
79
+ environmentId: id.optional(),
80
+ resources: resources.partial().optional(),
81
+ nodeId: id.optional(),
82
+ parentId: z.string().regex(/^[\da-f]{64}$/u).optional(),
83
+ /** 仅可信服务提交工具类别;其节点资源与普通 Worker 分开核算。 */
84
+ capacityClass: z.enum(["default", "tool"]).default("default"),
85
+ /** 使用已登记服务的单次预热池。 */
86
+ warmPool: z.literal(true).optional(),
87
+ /** 同一 kind + key 的跨节点上限,部署变更期间采用最小活动值。 */
88
+ concurrency: z.strictObject({
89
+ key: id,
90
+ limit: z.number().int().min(1).max(1e3)
91
+ }).optional(),
92
+ reuseKey: z.string().min(1).max(200).optional(),
93
+ queueTimeoutMs: timeout.default(3e5),
94
+ runTimeoutMs: timeout.default(3e5)
95
+ }).refine((value) => !value.parentId || !value.nodeId && !value.environmentId && !value.resources && !value.reuseKey && value.capacityClass === "default" && !value.concurrency).refine((value) => !value.warmPool || value.capacityClass === "tool" && value.environmentId && value.concurrency && !value.resources);
96
+ var warmPoolSchema = z.strictObject({
97
+ key: id,
98
+ kind: id,
99
+ environmentId: id,
100
+ minReady: z.number().int().min(0).max(1e3),
101
+ maxRunning: z.number().int().min(1).max(1e3),
102
+ maxTotal: z.number().int().min(1).max(1e3)
103
+ }).refine((value) => value.minReady <= value.maxTotal && value.maxRunning <= value.maxTotal);
104
+ var runnerKind = id;
105
+
106
+ // messages/en-US.json
107
+ var en_US_default = {
108
+ runner_notInitialized: "Runner is not initialized",
109
+ runner_alreadyInitialized: "Runner is already initialized or starting",
110
+ runner_invalid: "Invalid execution options",
111
+ runner_notFound: "Execution or environment not found",
112
+ runner_conflict: "The execution key has different options or execution state changed",
113
+ runner_unavailable: "Managed execution is unavailable",
114
+ runner_capacity: "Execution queue or resource limit reached",
115
+ runner_leaseLost: "The execution lease expired",
116
+ runner_cleanupUnconfirmed: "Unable to verify release of all execution resources"
117
+ };
118
+
119
+ // messages/ja-JP.json
120
+ var ja_JP_default = {
121
+ runner_notInitialized: "Runner \u306F\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093",
122
+ runner_alreadyInitialized: "Runner \u306F\u8D77\u52D5\u4E2D\u307E\u305F\u306F\u521D\u671F\u5316\u6E08\u307F\u3067\u3059",
123
+ runner_invalid: "\u5B9F\u884C\u30AA\u30D7\u30B7\u30E7\u30F3\u304C\u7121\u52B9\u3067\u3059",
124
+ runner_notFound: "\u5B9F\u884C\u30BF\u30B9\u30AF\u307E\u305F\u306F\u5B9F\u884C\u74B0\u5883\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093",
125
+ runner_conflict: "\u5B9F\u884C\u30AD\u30FC\u306E\u30AA\u30D7\u30B7\u30E7\u30F3\u304C\u7570\u306A\u308B\u304B\u3001\u5B9F\u884C\u72B6\u614B\u304C\u5909\u66F4\u3055\u308C\u307E\u3057\u305F",
126
+ runner_unavailable: "\u5B9F\u884C\u30B5\u30FC\u30D3\u30B9\u3092\u5229\u7528\u3067\u304D\u307E\u305B\u3093",
127
+ runner_capacity: "\u5B9F\u884C\u30AD\u30E5\u30FC\u307E\u305F\u306F\u30EA\u30BD\u30FC\u30B9\u306E\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F",
128
+ runner_leaseLost: "\u5B9F\u884C\u306E\u30EA\u30FC\u30B9\u304C\u671F\u9650\u5207\u308C\u3067\u3059",
129
+ runner_cleanupUnconfirmed: "\u5B9F\u884C\u30EA\u30BD\u30FC\u30B9\u306E\u89E3\u653E\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093"
130
+ };
131
+
132
+ // messages/zh-CN.json
133
+ var zh_CN_default = {
134
+ runner_notInitialized: "Runner \u5C1A\u672A\u521D\u59CB\u5316",
135
+ runner_alreadyInitialized: "Runner \u6B63\u5728\u521D\u59CB\u5316\u6216\u5DF2\u7ECF\u542F\u52A8",
136
+ runner_invalid: "\u8FD0\u884C\u53C2\u6570\u65E0\u6548",
137
+ runner_notFound: "\u672A\u627E\u5230\u6267\u884C\u4EFB\u52A1\u6216\u8FD0\u884C\u73AF\u5883",
138
+ runner_conflict: "\u76F8\u540C\u6267\u884C\u6807\u8BC6\u5DF2\u7ED1\u5B9A\u4E0D\u540C\u8BF7\u6C42\uFF0C\u6216\u8FD0\u884C\u72B6\u6001\u53D1\u751F\u53D8\u5316",
139
+ runner_unavailable: "\u6267\u884C\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528",
140
+ runner_capacity: "\u8FD0\u884C\u961F\u5217\u6216\u8D44\u6E90\u4E0A\u9650\u5DF2\u8FBE\u5230",
141
+ runner_leaseLost: "\u5F53\u524D\u6267\u884C\u79DF\u7EA6\u5DF2\u5931\u6548",
142
+ runner_cleanupUnconfirmed: "\u65E0\u6CD5\u786E\u8BA4\u6240\u6709\u8FD0\u884C\u8D44\u6E90\u5747\u5DF2\u91CA\u653E"
143
+ };
144
+
145
+ // src/runner-i18n.ts
146
+ var runnerM = core.i18n.createMessageGetter({ "zh-CN": zh_CN_default, "en-US": en_US_default, "ja-JP": ja_JP_default });
147
+ var HaiRunnerError = core.error.buildHaiErrorsDef("runner", {
148
+ NOT_INITIALIZED: "010:503",
149
+ ALREADY_INITIALIZED: "011:409",
150
+ INVALID: "020:400",
151
+ NOT_FOUND: "021:404",
152
+ CONFLICT: "022:409",
153
+ UNAVAILABLE: "023:503",
154
+ CAPACITY: "024:429",
155
+ LEASE_LOST: "025:409",
156
+ CLEANUP_UNCONFIRMED: "026:503"
157
+ });
158
+
159
+ // src/runner-container.ts
160
+ var execute = promisify(execFile);
161
+ var containerId = /^[\da-f]{64}$/u;
162
+ var envKey = /^[A-Z_]\w*$/iu;
163
+ var MAX_FILE_BYTES = 50 * 1024 * 1024;
164
+ var artifactName = /^[\da-z][\w.-]{0,127}$/iu;
165
+ var fileName = /^[\w.][\w.-]{0,95}$/u;
166
+ var argument = z.string().max(8192).refine((value) => !value.includes("\0"));
167
+ var launchSchema = z.strictObject({
168
+ command: argument.refine((value) => value.length > 0 && !value.startsWith("-")),
169
+ args: z.array(argument).max(128).default([]),
170
+ env: z.record(z.string().regex(envKey), z.string().max(64e3).refine((value) => !/[\r\n\0]/u.test(value))).default({}),
171
+ files: z.record(z.string().regex(fileName).refine((value) => value !== "." && value !== ".."), z.string().max(128e3)).default({}),
172
+ inputs: z.record(z.string().regex(artifactName), z.string().max(4 * Math.ceil(MAX_FILE_BYTES / 3))).optional(),
173
+ cwd: z.string().max(1024).default("/workspace")
174
+ });
175
+ var READ_ARTIFACT = `
176
+ import os, stat, sys
177
+ root = os.open('/output', os.O_DIRECTORY | os.O_NOFOLLOW)
178
+ fd = os.open(sys.argv[1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=root)
179
+ limit = int(sys.argv[2])
180
+ meta = os.fstat(fd)
181
+ if not stat.S_ISREG(meta.st_mode) or meta.st_size > limit:
182
+ raise SystemExit(2)
183
+ with os.fdopen(fd, 'rb') as item:
184
+ remaining = limit + 1
185
+ while remaining > 0:
186
+ data = item.read(min(65536, remaining))
187
+ if not data:
188
+ break
189
+ remaining -= len(data)
190
+ if remaining == 0:
191
+ raise SystemExit(2)
192
+ sys.stdout.buffer.write(data)
193
+ `;
194
+ async function cli(engine, args) {
195
+ const result = await execute(engine, args, { timeout: 15e3, maxBuffer: 65536, windowsHide: true });
196
+ return result.stdout.trim();
197
+ }
198
+ function invalid() {
199
+ return err(HaiRunnerError.INVALID, runnerM("runner_invalid"));
200
+ }
201
+ function unavailable() {
202
+ return err(HaiRunnerError.UNAVAILABLE, runnerM("runner_unavailable"));
203
+ }
204
+ function uncertain() {
205
+ return err(HaiRunnerError.CLEANUP_UNCONFIRMED, runnerM("runner_cleanupUnconfirmed"));
206
+ }
207
+ var stageName = /^hai-runner-[a-z\d]{6}$/iu;
208
+ async function stagingRoot() {
209
+ const path = await realpath(tmpdir());
210
+ const info = await stat(path, { bigint: true });
211
+ return { path, key: `${path}:${info.dev}:${info.ino}` };
212
+ }
213
+ async function engineKey(engine) {
214
+ const root = await stagingRoot();
215
+ const host = (await readFile("/etc/machine-id", "utf8")).trim();
216
+ if (!/^[\da-f]{32}$/u.test(host))
217
+ throw new Error("Host identity unavailable");
218
+ const value = engine === "podman" ? await cli(engine, ["info", "--format", "{{.Host.ServiceIsRemote}} {{.Store.GraphRoot}}"]) : await cli(engine, ["info", "--format", "{{.ID}}"]);
219
+ if (engine === "podman") {
220
+ if (!value.startsWith("false "))
221
+ throw new Error("Podman identity unavailable");
222
+ const storage = await stat(value.slice(6), { bigint: true });
223
+ return createHash("sha256").update(JSON.stringify([
224
+ host,
225
+ engine,
226
+ root.key,
227
+ value,
228
+ String(storage.dev),
229
+ String(storage.ino)
230
+ ])).digest("hex");
231
+ }
232
+ if (!value || value === "<no value>")
233
+ throw new Error("Docker identity unavailable");
234
+ return createHash("sha256").update(JSON.stringify([host, engine, root.key, value])).digest("hex");
235
+ }
236
+ async function containerAbsent(engine, id2) {
237
+ return !await cli(engine, [
238
+ "container",
239
+ "ls",
240
+ "--all",
241
+ "--no-trunc",
242
+ "--filter",
243
+ `id=${id2}`,
244
+ "--format",
245
+ "{{.ID}}"
246
+ ]);
247
+ }
248
+ async function identityOf(path) {
249
+ const info = await lstat(path, { bigint: true });
250
+ if (!info.isDirectory())
251
+ throw new Error("Runner staging path is invalid");
252
+ return `${info.dev}:${info.ino}`;
253
+ }
254
+ async function confirmContainerReleased(record) {
255
+ if (!containerId.test(record.id) || !stageName.test(record.stageName))
256
+ return uncertain();
257
+ try {
258
+ if (record.engineKey !== await engineKey(record.engine) || !await containerAbsent(record.engine, record.id))
259
+ return uncertain();
260
+ const path = join((await stagingRoot()).path, record.stageName);
261
+ let actual;
262
+ try {
263
+ actual = await identityOf(path);
264
+ } catch (error) {
265
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
266
+ return uncertain();
267
+ }
268
+ if (actual) {
269
+ if (actual !== record.stageKey)
270
+ return uncertain();
271
+ for (const name of ["input", "configuration"]) {
272
+ const directory = join(path, name);
273
+ try {
274
+ if (!(await lstat(directory)).isDirectory())
275
+ return uncertain();
276
+ await chmod(directory, 448);
277
+ } catch (error) {
278
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
279
+ return uncertain();
280
+ }
281
+ }
282
+ await rm(path, { recursive: true, force: true });
283
+ }
284
+ return record.engineKey === await engineKey(record.engine) && await containerAbsent(record.engine, record.id) ? ok(void 0) : uncertain();
285
+ } catch {
286
+ return uncertain();
287
+ }
288
+ }
289
+ var STANDBY = `
290
+ import json, os, sys
291
+ if os.listdir('/input') or os.listdir('/output'):
292
+ raise SystemExit(2)
293
+ os.write(1, b'+')
294
+ if os.read(0, 1) != b'+':
295
+ raise SystemExit(2)
296
+ with open('/run/hai-config/warm-launch.json', 'rb') as configuration:
297
+ data = configuration.read(524289)
298
+ if len(data) > 524288:
299
+ raise SystemExit(2)
300
+ command, *args = json.loads(data)
301
+ os.execvp(command, [command, *args])
302
+ `;
303
+ function decodeInputs(inputs, resources2) {
304
+ const budget = Math.min(MAX_FILE_BYTES, resources2.scratchMiB * 1024 * 1024);
305
+ const entries = Object.entries(inputs);
306
+ if (entries.length > 8 || entries.reduce((total, [, value]) => total + value.length, 0) > 4 * Math.ceil(budget / 3) + 24)
307
+ return void 0;
308
+ const decoded = entries.map(([name, value]) => ({
309
+ name,
310
+ value,
311
+ bytes: Buffer.from(value, "base64")
312
+ }));
313
+ return decoded.some((item) => item.bytes.toString("base64") !== item.value) || decoded.reduce((total, item) => total + item.bytes.length, 0) > budget ? void 0 : decoded;
314
+ }
315
+ async function startContainer(engine, environment2, resources2, launch, signal, record, warm = false) {
316
+ const parsed = launchSchema.safeParse(launch);
317
+ if (!parsed.success || signal.aborted)
318
+ return invalid();
319
+ const options = parsed.data;
320
+ const cwd = posix.normalize(options.cwd);
321
+ if (cwd !== options.cwd || cwd !== "/workspace" && !cwd.startsWith("/workspace/") || cwd.includes("\0") || Object.keys(options.env).length > 128 || Object.keys(options.files).length > 32) {
322
+ return invalid();
323
+ }
324
+ const decoded = options.inputs === void 0 ? void 0 : decodeInputs(options.inputs, resources2);
325
+ if (options.inputs && !decoded)
326
+ return invalid();
327
+ let temporary;
328
+ let id2;
329
+ let envFile;
330
+ const readOnlyDirs = [];
331
+ let creationStarted = false;
332
+ async function removeContainer(container) {
333
+ try {
334
+ await cli(engine, [
335
+ "container",
336
+ "rm",
337
+ "--force",
338
+ "--volumes",
339
+ ...engine === "podman" ? ["--time", "1"] : [],
340
+ container
341
+ ]);
342
+ } catch {
343
+ }
344
+ try {
345
+ return await containerAbsent(engine, container);
346
+ } catch {
347
+ return false;
348
+ }
349
+ }
350
+ async function removeFiles() {
351
+ await Promise.all(readOnlyDirs.map((dir) => chmod(dir, 448)));
352
+ if (temporary)
353
+ await rm(temporary, { recursive: true, force: true });
354
+ }
355
+ try {
356
+ const mode = engine === "podman" ? await cli(engine, ["info", "--format", '{{.Host.CgroupsVersion}} {{join .Host.CgroupControllers ","}}']) : await cli(engine, ["info", "--format", "{{.OSType}} {{.CgroupVersion}} {{.CPUCfsQuota}} {{.MemoryLimit}} {{.PidsLimit}}"]);
357
+ if (engine === "podman" ? !mode.startsWith("v2 ") || !["cpu", "memory", "pids"].every((name) => mode.slice(3).split(",").includes(name)) : mode !== "linux 2 true true true") {
358
+ return unavailable();
359
+ }
360
+ const imageVolumes = await cli(engine, [
361
+ "image",
362
+ "inspect",
363
+ "--format",
364
+ "{{json .Config.Volumes}}",
365
+ environment2.image
366
+ ]);
367
+ if (imageVolumes !== "null" && imageVolumes !== "{}")
368
+ return unavailable();
369
+ if (environment2.networkPolicy === "internal") {
370
+ const isolated = await cli(engine, [
371
+ "network",
372
+ "inspect",
373
+ "--format",
374
+ "{{.Internal}}",
375
+ environment2.network
376
+ ]);
377
+ if (isolated !== "true")
378
+ return unavailable();
379
+ }
380
+ const key = await engineKey(engine);
381
+ temporary = await mkdtemp(join((await stagingRoot()).path, "hai-runner-"));
382
+ envFile = join(temporary, "environment");
383
+ const files = join(temporary, "configuration");
384
+ await writeFile(envFile, Object.entries({
385
+ HOME: "/state",
386
+ XDG_CACHE_HOME: "/state/cache",
387
+ XDG_CONFIG_HOME: "/state/config",
388
+ TMPDIR: "/tmp",
389
+ ...options.env
390
+ }).map(([key2, value]) => `${key2}=${value}`).join("\n"), { mode: 384, flag: "wx" });
391
+ await mkdir(files);
392
+ readOnlyDirs.push(files);
393
+ await Promise.all(Object.entries(options.files).map(([name, content]) => writeFile(join(files, name), content, { mode: 292, flag: "wx" })));
394
+ await chmod(files, 365);
395
+ const inputDir = join(temporary, "input");
396
+ if (decoded) {
397
+ await mkdir(inputDir);
398
+ readOnlyDirs.push(inputDir);
399
+ await Promise.all(decoded.map((item) => writeFile(join(inputDir, item.name), item.bytes, { mode: 292, flag: "wx" })));
400
+ await chmod(inputDir, 365);
401
+ }
402
+ if (signal.aborted) {
403
+ await removeFiles();
404
+ return invalid();
405
+ }
406
+ const state = Math.max(1, Math.floor(resources2.scratchMiB / 8));
407
+ const workspace = Math.max(1, Math.floor(resources2.scratchMiB / (decoded ? 8 : 2)));
408
+ const output = decoded ? Math.max(1, Math.floor(resources2.scratchMiB / 2)) : 0;
409
+ const scratch = resources2.scratchMiB - state - workspace - output;
410
+ if (scratch < 1) {
411
+ await removeFiles();
412
+ return invalid();
413
+ }
414
+ const mounts = [
415
+ ["state", state],
416
+ ["workspace", workspace],
417
+ ["tmp", scratch],
418
+ ...decoded ? [
419
+ ["output", output]
420
+ ] : []
421
+ ].flatMap(([name, size]) => ["--tmpfs", `/${name}:rw,nosuid,nodev,mode=1777,size=${size}m`]);
422
+ creationStarted = true;
423
+ const created = await cli(engine, [
424
+ "container",
425
+ "create",
426
+ "--pull=never",
427
+ "--interactive",
428
+ "--user=1000:1000",
429
+ "--read-only",
430
+ "--cap-drop=ALL",
431
+ "--security-opt=no-new-privileges",
432
+ "--cgroupns=private",
433
+ "--ipc=private",
434
+ "--shm-size=1m",
435
+ `--cpus=${resources2.cpu}`,
436
+ `--memory=${resources2.memoryMiB}m`,
437
+ `--memory-swap=${resources2.memoryMiB}m`,
438
+ `--pids-limit=${resources2.pids}`,
439
+ `--network=${environment2.network ?? "none"}`,
440
+ ...engine === "podman" ? ["--read-only-tmpfs=false", "--http-proxy=false"] : [],
441
+ "--env-file",
442
+ envFile,
443
+ "--mount",
444
+ `type=bind,source=${files},target=/run/hai-config,readonly`,
445
+ ...mounts,
446
+ ...decoded ? ["--mount", `type=bind,source=${inputDir},target=/input,readonly`] : [],
447
+ "--entrypoint=/bin/sh",
448
+ environment2.image,
449
+ "-c",
450
+ 'cp -R /run/hai-config/. /state/ && cd "$1" && shift && exec "$@"',
451
+ "hai-runner",
452
+ cwd,
453
+ options.command,
454
+ ...options.args
455
+ ]);
456
+ if (!containerId.test(created))
457
+ return uncertain();
458
+ id2 = created;
459
+ await rm(envFile, { force: true });
460
+ const receipt = await record({
461
+ id: id2,
462
+ engine,
463
+ engineKey: key,
464
+ stageName: basename(temporary),
465
+ stageKey: await identityOf(temporary)
466
+ });
467
+ if (signal.aborted || !receipt.success) {
468
+ const stopped = await removeContainer(id2);
469
+ if (stopped)
470
+ await removeFiles();
471
+ if (!stopped)
472
+ return uncertain();
473
+ return receipt.success ? invalid() : receipt;
474
+ }
475
+ const child = spawn(engine, [
476
+ "container",
477
+ "start",
478
+ "--attach",
479
+ "--interactive",
480
+ id2
481
+ ], { stdio: "pipe", windowsHide: true });
482
+ child.stdin.on("error", () => {
483
+ });
484
+ const completion = new Promise((resolve) => {
485
+ child.once("error", () => resolve({ code: null, signal: null }));
486
+ child.once("close", (code, exited) => resolve({ code, signal: exited }));
487
+ });
488
+ let closing;
489
+ const stop = () => {
490
+ closing ??= (async () => {
491
+ if (!await removeContainer(created))
492
+ return uncertain();
493
+ child.kill();
494
+ try {
495
+ await removeFiles();
496
+ return ok(void 0);
497
+ } catch {
498
+ return uncertain();
499
+ }
500
+ })();
501
+ return closing;
502
+ };
503
+ let activated = false;
504
+ const process3 = {
505
+ stdin: child.stdin,
506
+ stdout: child.stdout,
507
+ stderr: child.stderr,
508
+ wait: () => completion,
509
+ async activate(launch2, activationSignal, authorize) {
510
+ if (!warm || activated || closing || activationSignal.aborted)
511
+ return invalid();
512
+ activated = true;
513
+ const parsed2 = launchSchema.safeParse(launch2);
514
+ if (!parsed2.success || parsed2.data.cwd !== "/workspace" || Object.keys(parsed2.data.env).length || Object.keys(parsed2.data.files).length || !parsed2.data.inputs) {
515
+ return invalid();
516
+ }
517
+ const staged = decodeInputs(parsed2.data.inputs, resources2);
518
+ const request = JSON.stringify([parsed2.data.command, ...parsed2.data.args]);
519
+ if (!staged || Buffer.byteLength(request) > 524288)
520
+ return invalid();
521
+ try {
522
+ await chmod(inputDir, 493);
523
+ await chmod(files, 493);
524
+ const writes = await Promise.allSettled([
525
+ ...staged.map((item) => writeFile(join(inputDir, item.name), item.bytes, { mode: 292, flag: "wx" })),
526
+ writeFile(join(files, "warm-launch.json"), request, { mode: 292, flag: "wx" })
527
+ ]);
528
+ await chmod(inputDir, 365);
529
+ await chmod(files, 365);
530
+ if (writes.some((item) => item.status === "rejected") || !await authorize() || activationSignal.aborted || closing)
531
+ return invalid();
532
+ await new Promise((resolve, reject) => {
533
+ child.stdin.write("+", (error) => error ? reject(error) : resolve());
534
+ });
535
+ return ok(process3);
536
+ } catch {
537
+ return unavailable();
538
+ }
539
+ },
540
+ async artifact(name, maxBytes) {
541
+ if (!decoded || closing || signal.aborted || !artifactName.test(name) || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_FILE_BYTES) {
542
+ return invalid();
543
+ }
544
+ try {
545
+ const { stdout } = await execute(engine, [
546
+ "container",
547
+ "exec",
548
+ "--user=1000:1000",
549
+ created,
550
+ "/usr/bin/python3",
551
+ "-I",
552
+ "-S",
553
+ "-c",
554
+ READ_ARTIFACT,
555
+ name,
556
+ String(maxBytes)
557
+ ], {
558
+ encoding: "buffer",
559
+ timeout: 15e3,
560
+ maxBuffer: maxBytes,
561
+ windowsHide: true,
562
+ signal
563
+ });
564
+ return stdout.length <= maxBytes ? ok(stdout.toString("base64")) : invalid();
565
+ } catch {
566
+ return invalid();
567
+ }
568
+ },
569
+ close: stop
570
+ };
571
+ if (warm) {
572
+ const readySignal = AbortSignal.any([
573
+ signal,
574
+ AbortSignal.timeout(15e3)
575
+ ]);
576
+ try {
577
+ const [chunk] = await once(child.stdout, "data", { signal: readySignal });
578
+ child.stdout.pause();
579
+ if (!Buffer.isBuffer(chunk) || chunk.length !== 1 || chunk[0] !== 43)
580
+ throw new Error("Invalid warm readiness");
581
+ } catch {
582
+ return (await stop()).success ? unavailable() : uncertain();
583
+ }
584
+ }
585
+ return ok(process3);
586
+ } catch {
587
+ if (envFile) {
588
+ try {
589
+ await rm(envFile, { force: true });
590
+ } catch {
591
+ }
592
+ }
593
+ if (id2 && await removeContainer(id2))
594
+ creationStarted = false;
595
+ if (!creationStarted) {
596
+ try {
597
+ await removeFiles();
598
+ } catch {
599
+ return uncertain();
600
+ }
601
+ }
602
+ return creationStarted ? uncertain() : unavailable();
603
+ }
604
+ }
605
+ async function prepareWarmContainer(engine, environment2, signal, record) {
606
+ if (environment2.networkPolicy !== "offline")
607
+ return invalid();
608
+ return startContainer(engine, environment2, environment2.resources, {
609
+ command: "/usr/bin/python3",
610
+ args: ["-I", "-S", "-c", STANDBY],
611
+ inputs: {}
612
+ }, signal, record, true);
613
+ }
614
+ var processIdentity = z.strictObject({
615
+ pid: z.number().int().positive().max(2 ** 31 - 1),
616
+ hostKey: z.string().regex(/^[a-f\d]{64}$/u),
617
+ started: z.string().regex(/^\d+$/u).max(20)
618
+ });
619
+ async function hostKey() {
620
+ const [machine, boot, namespace] = await Promise.all([
621
+ readFile("/etc/machine-id", "utf8"),
622
+ readFile("/proc/sys/kernel/random/boot_id", "utf8"),
623
+ readlink("/proc/self/ns/pid")
624
+ ]);
625
+ if (!/^[a-f\d]{32}$/u.test(machine.trim()) || !/^[a-f\d-]{36}$/u.test(boot.trim()) || !/^pid:\[\d+\]$/u.test(namespace)) {
626
+ throw new Error("Runner host identity unavailable");
627
+ }
628
+ return createHash("sha256").update(JSON.stringify([
629
+ machine.trim(),
630
+ boot.trim(),
631
+ namespace
632
+ ])).digest("hex");
633
+ }
634
+ async function startTime(pid) {
635
+ const raw = await readFile(`/proc/${pid}/stat`, "utf8");
636
+ const index = raw.lastIndexOf(") ");
637
+ const field = index < 0 ? void 0 : raw.slice(index + 2).split(/\s+/u)[19];
638
+ if (!field || !/^\d+$/u.test(field))
639
+ throw new Error("Runner process identity unavailable");
640
+ return field;
641
+ }
642
+ async function readRunnerProcess() {
643
+ if (process.platform !== "linux")
644
+ return void 0;
645
+ try {
646
+ const [host, started] = await Promise.all([
647
+ hostKey(),
648
+ startTime(process.pid)
649
+ ]);
650
+ return { pid: process.pid, hostKey: host, started };
651
+ } catch {
652
+ return void 0;
653
+ }
654
+ }
655
+ async function confirmRunnerProcessReleased(identity) {
656
+ const record = processIdentity.safeParse(identity);
657
+ if (!record.success || process.platform !== "linux")
658
+ return false;
659
+ const { pid, hostKey: previous, started } = record.data;
660
+ try {
661
+ if (previous !== await hostKey())
662
+ return false;
663
+ try {
664
+ return await startTime(pid) !== started;
665
+ } catch (error) {
666
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
667
+ return false;
668
+ try {
669
+ process.kill(pid, 0);
670
+ return false;
671
+ } catch (cause) {
672
+ return cause instanceof Error && "code" in cause && cause.code === "ESRCH";
673
+ }
674
+ }
675
+ } catch {
676
+ return false;
677
+ }
678
+ }
679
+ var EXECUTIONS = "hai_runner_executions";
680
+ var EVENTS = "hai_runner_events";
681
+ var GATE = "hai_runner_gate";
682
+ var ACTIVE = "(state IN ('running', 'cancelling') OR (state = 'unknown' AND reconciled_at IS NULL))";
683
+ var RETENTION_STATES = [
684
+ "succeeded",
685
+ "failed",
686
+ "cancelled",
687
+ "unknown"
688
+ ];
689
+ var RETENTION_BATCH = 128;
690
+ var MAX_EVENT_BYTES = 16 * 1024 * 1024;
691
+ var text = { type: "TEXT", notNull: true };
692
+ var nullableText = { type: "TEXT" };
693
+ var integer = { type: "BIGINT", notNull: true };
694
+ var payload = { type: "JSON" };
695
+ var executions = {
696
+ id: { ...text, primaryKey: true },
697
+ scope_key: text,
698
+ reuse_key: { ...nullableText, unique: true },
699
+ request_key: text,
700
+ kind: text,
701
+ request_json: payload,
702
+ environment_key: nullableText,
703
+ state: text,
704
+ reason: nullableText,
705
+ owner_node: nullableText,
706
+ owner_generation: nullableText,
707
+ owner_process_json: payload,
708
+ pinned_node: nullableText,
709
+ pinned_generation: nullableText,
710
+ parent_id: nullableText,
711
+ leased_until: integer,
712
+ queue_until: integer,
713
+ run_until: integer,
714
+ event_sequence: integer,
715
+ event_bytes: { ...integer, defaultValue: 0 },
716
+ cpu_milli: integer,
717
+ memory_mib: integer,
718
+ pids: integer,
719
+ scratch_mib: integer,
720
+ result_json: payload,
721
+ created_at: integer,
722
+ updated_at: integer,
723
+ container_json: payload,
724
+ reconciliation_json: payload,
725
+ reconciled_at: { type: "BIGINT" },
726
+ capacity_class: text,
727
+ concurrency_key: nullableText,
728
+ concurrency_limit: integer,
729
+ pool_fingerprint: nullableText,
730
+ pool_limit: { type: "BIGINT" },
731
+ warm_state: nullableText
732
+ };
733
+ function clock() {
734
+ if (reldb.config?.type === "postgresql")
735
+ return "CAST(FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000) AS BIGINT)";
736
+ if (reldb.config?.type === "mysql")
737
+ return "CAST(UNIX_TIMESTAMP(SYSDATE(3)) * 1000 AS UNSIGNED)";
738
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
739
+ }
740
+ function canonical(value) {
741
+ if (Array.isArray(value))
742
+ return `[${value.map(canonical).join(",")}]`;
743
+ if (value !== null && typeof value === "object")
744
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
745
+ return JSON.stringify(value);
746
+ }
747
+ function hash(value) {
748
+ return createHash("sha256").update(canonical(value)).digest("hex");
749
+ }
750
+ function databaseValue(value) {
751
+ return typeof value === "string" ? JSON.parse(value) : value;
752
+ }
753
+ function project(row, environmentId) {
754
+ return {
755
+ id: row.id,
756
+ status: row.state,
757
+ owner: row.owner_node && row.owner_generation ? { nodeId: row.owner_node, generation: row.owner_generation } : void 0,
758
+ environmentId,
759
+ output: row.result_json === null ? void 0 : databaseValue(row.result_json).value,
760
+ failure: row.reason ?? void 0,
761
+ createdAt: Number(row.created_at),
762
+ updatedAt: Number(row.updated_at),
763
+ reconciledAt: row.reconciled_at === null ? void 0 : Number(row.reconciled_at)
764
+ };
765
+ }
766
+ function projectRequest(row) {
767
+ return project(row, databaseValue(row.request_json).environment?.id);
768
+ }
769
+ function unavailable2() {
770
+ return err(HaiRunnerError.UNAVAILABLE, runnerM("runner_unavailable"));
771
+ }
772
+ function lost() {
773
+ return err(HaiRunnerError.LEASE_LOST, runnerM("runner_leaseLost"));
774
+ }
775
+ function missing() {
776
+ return err(HaiRunnerError.NOT_FOUND, runnerM("runner_notFound"));
777
+ }
778
+ function conflict() {
779
+ return err(HaiRunnerError.CONFLICT, runnerM("runner_conflict"));
780
+ }
781
+ function overflow() {
782
+ return err(HaiRunnerError.CAPACITY, runnerM("runner_capacity"));
783
+ }
784
+ function data(result) {
785
+ if (!result.success)
786
+ throw new Error("Runner database operation failed", { cause: result.error });
787
+ return result.data;
788
+ }
789
+ async function serialized(action) {
790
+ const response = await reldb.tx.wrap(async (tx) => {
791
+ data(await tx.execute(`UPDATE ${GATE} SET revision = revision + 1 WHERE id = 1`));
792
+ return action(tx);
793
+ });
794
+ return response.success ? response.data : unavailable2();
795
+ }
796
+ async function find(sql, id2, scopeKey) {
797
+ return data(await sql.get(`SELECT * FROM ${EXECUTIONS} WHERE id = ? AND scope_key = ?`, [id2, scopeKey]));
798
+ }
799
+ function columns(resources2) {
800
+ return resources2 ? [
801
+ Math.ceil(resources2.cpu * 1e3),
802
+ resources2.memoryMiB,
803
+ resources2.pids,
804
+ resources2.scratchMiB
805
+ ] : [0, 0, 0, 0];
806
+ }
807
+ function within(actual, ceiling) {
808
+ return actual.cpu <= ceiling.cpu && actual.memoryMiB <= ceiling.memoryMiB && actual.pids <= ceiling.pids && actual.scratchMiB <= ceiling.scratchMiB;
809
+ }
810
+ async function readyStore() {
811
+ if (!reldb.isInitialized)
812
+ return unavailable2();
813
+ const addedColumns = ["event_bytes", "owner_process_json"];
814
+ for (const [name, def] of [
815
+ [
816
+ GATE,
817
+ {
818
+ id: { ...integer, primaryKey: true },
819
+ revision: integer
820
+ }
821
+ ],
822
+ [EXECUTIONS, executions],
823
+ [
824
+ EVENTS,
825
+ {
826
+ id: { ...text, primaryKey: true },
827
+ execution_id: text,
828
+ sequence_number: integer,
829
+ event_json: payload,
830
+ created_at: integer
831
+ }
832
+ ]
833
+ ]) {
834
+ const result = await reldb.ddl.createTable(name, def);
835
+ if (!result.success) {
836
+ const existing2 = await reldb.schemaColumns(name);
837
+ if (!existing2.success || Object.keys(def).some((column) => !existing2.data.includes(column) && !(name === EXECUTIONS && addedColumns.includes(column))))
838
+ return unavailable2();
839
+ }
840
+ }
841
+ const existing = await reldb.schemaColumns(EXECUTIONS);
842
+ if (!existing.success)
843
+ return unavailable2();
844
+ for (const column of addedColumns.filter((column2) => !existing.data.includes(column2))) {
845
+ const added = await reldb.ddl.addColumn(EXECUTIONS, column, executions[column]);
846
+ if (!added.success) {
847
+ const refreshed = await reldb.schemaColumns(EXECUTIONS);
848
+ if (!refreshed.success || !refreshed.data.includes(column))
849
+ return unavailable2();
850
+ }
851
+ }
852
+ for (const [table, name, fields] of [
853
+ [
854
+ EXECUTIONS,
855
+ "hai_runner_queue_idx",
856
+ ["state", "kind", "created_at"]
857
+ ],
858
+ [
859
+ EXECUTIONS,
860
+ "hai_runner_owner_idx",
861
+ ["owner_node", "state", "leased_until"]
862
+ ],
863
+ [
864
+ EXECUTIONS,
865
+ "hai_runner_scope_idx",
866
+ ["scope_key", "capacity_class", "state"]
867
+ ],
868
+ [
869
+ EXECUTIONS,
870
+ "hai_runner_concurrency_idx",
871
+ ["concurrency_key", "state"]
872
+ ],
873
+ [
874
+ EXECUTIONS,
875
+ "hai_runner_parent_idx",
876
+ ["parent_id"]
877
+ ],
878
+ [
879
+ EXECUTIONS,
880
+ "hai_runner_retention_idx",
881
+ [
882
+ "state",
883
+ "reconciled_at",
884
+ "updated_at",
885
+ "id"
886
+ ]
887
+ ],
888
+ [
889
+ EXECUTIONS,
890
+ "hai_runner_queue_due_idx",
891
+ ["state", "queue_until"]
892
+ ],
893
+ [
894
+ EXECUTIONS,
895
+ "hai_runner_lease_due_idx",
896
+ ["state", "leased_until"]
897
+ ],
898
+ [
899
+ EXECUTIONS,
900
+ "hai_runner_run_due_idx",
901
+ ["state", "run_until"]
902
+ ],
903
+ [
904
+ EVENTS,
905
+ "hai_runner_event_cursor_idx",
906
+ ["execution_id", "sequence_number"]
907
+ ]
908
+ ]) {
909
+ const result = await reldb.ddl.createIndex(table, name, { columns: fields });
910
+ if (!result.success) {
911
+ if (reldb.config?.type !== "mysql")
912
+ return unavailable2();
913
+ const existing2 = await reldb.sql.query(
914
+ "SELECT COLUMN_NAME AS column_name FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ? ORDER BY SEQ_IN_INDEX",
915
+ [table, name]
916
+ );
917
+ if (!existing2.success || JSON.stringify(existing2.data.map((row) => row.column_name)) !== JSON.stringify(fields))
918
+ return unavailable2();
919
+ }
920
+ }
921
+ const syntax = reldb.config?.type === "mysql" ? " ON DUPLICATE KEY UPDATE id = id" : " ON CONFLICT (id) DO NOTHING";
922
+ const seed = await reldb.sql.execute(`INSERT INTO ${GATE} (id, revision) VALUES (1, 0)${syntax}`);
923
+ return seed.success ? ok(void 0) : unavailable2();
924
+ }
925
+ function makeStore(config, owner, ownerProcess) {
926
+ const envById = new Map(config.environments.map((item) => [item.id, item]));
927
+ const envKeys = config.environments.filter((item) => !item.architecture || item.architecture === process.arch).map((item) => hash(item));
928
+ const now = clock();
929
+ const resultValue = reldb.config?.type === "postgresql" ? "CAST(? AS JSONB)" : reldb.config?.type === "mysql" ? "CAST(CONVERT(? USING utf8mb4) AS JSON)" : "?";
930
+ const statementTime = reldb.config?.type === "postgresql" ? "CAST(FLOOR(EXTRACT(EPOCH FROM statement_timestamp()) * 1000) AS BIGINT)" : reldb.config?.type === "mysql" ? "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS SIGNED)" : now;
931
+ const processJson = ownerProcess ? JSON.stringify(ownerProcess) : null;
932
+ const environmentPath = reldb.json.extract("request_json", "$.environment.id");
933
+ const environmentProjection = reldb.config?.type === "mysql" ? `JSON_UNQUOTE(${environmentPath.sql})` : reldb.config?.type === "postgresql" ? "request_json #>> ?::text[]" : environmentPath.sql;
934
+ async function summary(id2, scopeKey) {
935
+ const row = data(await reldb.sql.get(`
936
+ SELECT id, state, reason, owner_node, owner_generation, result_json, created_at, updated_at, reconciled_at,
937
+ ${environmentProjection} AS environment_id
938
+ FROM ${EXECUTIONS} WHERE id = ? AND scope_key = ?`, [...environmentPath.params, id2, scopeKey]));
939
+ return row ? project(row, row.environment_id ?? void 0) : void 0;
940
+ }
941
+ async function parentSettled(sql, row) {
942
+ if (!row.parent_id)
943
+ return true;
944
+ const parent = data(await sql.get(`
945
+ SELECT state, reconciled_at FROM ${EXECUTIONS}
946
+ WHERE id = ? AND owner_node = ? AND owner_generation = ? AND scope_key = ?`, [
947
+ row.parent_id,
948
+ row.owner_node,
949
+ row.owner_generation,
950
+ row.scope_key
951
+ ]));
952
+ return !!parent && (parent.state === "unknown" ? parent.reconciled_at !== null : ["succeeded", "failed", "cancelled"].includes(parent.state));
953
+ }
954
+ async function sweep(sql) {
955
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'failed', reason = 'queue-timeout', reuse_key = NULL, updated_at = ${now}
956
+ WHERE state = 'queued' AND queue_until <= ${now}`));
957
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'unknown', reason = 'owner-lost', updated_at = ${now}
958
+ WHERE state IN ('running', 'cancelling') AND leased_until <= ${now}`));
959
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'cancelling', reason = 'run-timeout', updated_at = ${now}
960
+ WHERE state = 'running' AND run_until <= ${now} AND leased_until > ${now}`));
961
+ }
962
+ async function expired(sql, limit) {
963
+ const pages = await Promise.all(RETENTION_STATES.map((state) => {
964
+ const endedAt = state === "unknown" ? "q.reconciled_at" : "q.updated_at";
965
+ return sql.query(`
966
+ SELECT q.id, ${endedAt} AS ended_at FROM ${EXECUTIONS} q
967
+ WHERE q.state = ? AND q.reconciled_at ${state === "unknown" ? "IS NOT NULL" : "IS NULL"}
968
+ AND ${endedAt} <= ${statementTime} - ?
969
+ AND NOT EXISTS (SELECT 1 FROM ${EXECUTIONS} child WHERE child.parent_id = q.id)
970
+ ORDER BY ${endedAt}, q.updated_at, q.id LIMIT ?`, [state, config.retentionMs, limit]);
971
+ }));
972
+ return pages.flatMap((page) => data(page)).sort((left, right) => Number(left.ended_at) - Number(right.ended_at)).slice(0, limit);
973
+ }
974
+ return {
975
+ async sweep() {
976
+ const due = await reldb.sql.get(`
977
+ SELECT id FROM ${EXECUTIONS} WHERE (state = 'queued' AND queue_until <= ${statementTime})
978
+ OR (state IN ('running', 'cancelling') AND leased_until <= ${statementTime})
979
+ OR (state = 'running' AND run_until <= ${statementTime}) LIMIT 1`);
980
+ if (!due.success)
981
+ return unavailable2();
982
+ if (!due.data)
983
+ return ok(void 0);
984
+ return serialized(async (sql) => {
985
+ await sweep(sql);
986
+ return ok(void 0);
987
+ });
988
+ },
989
+ async prune() {
990
+ try {
991
+ if (!(await expired(reldb.sql, 1)).length)
992
+ return ok(0);
993
+ } catch {
994
+ return unavailable2();
995
+ }
996
+ return serialized(async (sql) => {
997
+ const entries = await expired(sql, RETENTION_BATCH);
998
+ if (!entries.length)
999
+ return ok(0);
1000
+ const ids = entries.map((entry) => entry.id);
1001
+ const keys = ids.map(() => "?").join(", ");
1002
+ data(await sql.execute(`DELETE FROM ${EVENTS} WHERE execution_id IN (${keys})`, ids));
1003
+ const removed = data(await sql.execute(`DELETE FROM ${EXECUTIONS} WHERE id IN (${keys})`, ids));
1004
+ return ok(removed.changes);
1005
+ });
1006
+ },
1007
+ async submit(request, pool) {
1008
+ const scopeKey = hash([request.scope.tenantId, request.scope.userId]);
1009
+ const id2 = hash([scopeKey, request.kind, request.idempotencyKey ?? randomUUID()]);
1010
+ const reuse = request.reuseKey === void 0 ? null : hash([scopeKey, request.kind, request.reuseKey]);
1011
+ const requestKey = hash({
1012
+ kind: request.kind,
1013
+ input: request.input,
1014
+ environmentId: request.environmentId ?? null,
1015
+ resources: request.resources ?? null,
1016
+ parentId: request.parentId ?? null,
1017
+ nodeId: request.nodeId ?? null,
1018
+ queueTimeoutMs: request.queueTimeoutMs,
1019
+ capacityClass: request.capacityClass,
1020
+ concurrency: request.concurrency ?? null,
1021
+ warmPool: pool ? { ...pool } : null,
1022
+ runTimeoutMs: request.runTimeoutMs
1023
+ });
1024
+ return serialized(async (sql) => {
1025
+ const existing = await find(sql, id2, scopeKey) ?? (reuse ? data(await sql.get(`SELECT * FROM ${EXECUTIONS} WHERE reuse_key = ?`, [reuse])) : null);
1026
+ if (existing) {
1027
+ return existing.request_key === requestKey ? ok(projectRequest(existing)) : conflict();
1028
+ }
1029
+ const environment2 = request.environmentId ? envById.get(request.environmentId) : void 0;
1030
+ if (request.warmPool && (!pool || !environment2))
1031
+ return missing();
1032
+ if (request.environmentId && !environment2)
1033
+ return missing();
1034
+ const resources2 = environment2 ? {
1035
+ ...environment2.resources,
1036
+ ...request.resources
1037
+ } : request.resources ? {
1038
+ cpu: 1,
1039
+ memoryMiB: 64,
1040
+ pids: 16,
1041
+ scratchMiB: 16,
1042
+ ...request.resources
1043
+ } : void 0;
1044
+ if (environment2 && resources2 && !within(resources2, environment2.resources))
1045
+ return overflow();
1046
+ const parent = request.parentId ? await find(sql, request.parentId, scopeKey) : null;
1047
+ if (request.parentId && (!parent || parent.state !== "running"))
1048
+ return conflict();
1049
+ const parentFilter = request.parentId ? "IS NOT NULL" : "IS NULL";
1050
+ const queued = data(await sql.get(`SELECT COUNT(*) AS total FROM ${EXECUTIONS} WHERE state = 'queued' AND parent_id ${parentFilter}`));
1051
+ if (Number(queued?.total) >= config.maxQueued)
1052
+ return overflow();
1053
+ const saved = {
1054
+ scope: request.scope,
1055
+ input: request.input,
1056
+ environment: environment2,
1057
+ resources: resources2,
1058
+ runTimeoutMs: request.runTimeoutMs
1059
+ };
1060
+ data(await sql.execute(`INSERT INTO ${EXECUTIONS}
1061
+ (id, scope_key, reuse_key, request_key, kind, request_json, environment_key, state, pinned_node, pinned_generation, parent_id,
1062
+ leased_until, queue_until, run_until, event_sequence, cpu_milli, memory_mib, pids, scratch_mib, created_at, updated_at, capacity_class, concurrency_key, concurrency_limit, pool_fingerprint, pool_limit)
1063
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?, 0, ${now} + ?, 0, 0, ?, ?, ?, ?, ${now}, ${now}, ?, ?, ?, ?, ?)`, [
1064
+ id2,
1065
+ scopeKey,
1066
+ reuse,
1067
+ requestKey,
1068
+ request.kind,
1069
+ JSON.stringify(saved),
1070
+ environment2 ? hash(environment2) : null,
1071
+ parent?.owner_node ?? request.nodeId ?? null,
1072
+ parent?.owner_generation ?? null,
1073
+ request.parentId ?? null,
1074
+ request.queueTimeoutMs,
1075
+ ...columns(resources2),
1076
+ request.capacityClass,
1077
+ request.concurrency ? hash([request.kind, request.concurrency.key]) : null,
1078
+ request.concurrency?.limit ?? 0,
1079
+ pool && environment2 ? hash({
1080
+ pool: { ...pool },
1081
+ environment: environment2
1082
+ }) : null,
1083
+ pool?.maxTotal ?? null
1084
+ ]));
1085
+ const row = await find(sql, id2, scopeKey);
1086
+ if (!row)
1087
+ throw new Error("Newly inserted execution could not be read");
1088
+ return ok(projectRequest(row));
1089
+ });
1090
+ },
1091
+ async find(scope, kind, key) {
1092
+ try {
1093
+ const scopeKey = hash([scope.tenantId, scope.userId]);
1094
+ return ok(await summary(hash([scopeKey, kind, key]), scopeKey));
1095
+ } catch {
1096
+ return unavailable2();
1097
+ }
1098
+ },
1099
+ async get(scope, id2) {
1100
+ try {
1101
+ const found = await summary(id2, hash([scope.tenantId, scope.userId]));
1102
+ return found ? ok(found) : missing();
1103
+ } catch {
1104
+ return unavailable2();
1105
+ }
1106
+ },
1107
+ async cancel(scope, id2) {
1108
+ return serialized(async (sql) => {
1109
+ const row = await find(sql, id2, hash([scope.tenantId, scope.userId]));
1110
+ if (!row)
1111
+ return missing();
1112
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'cancelled', reuse_key = NULL, updated_at = ${now} WHERE id = ? AND state = 'queued'`, [id2]));
1113
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'cancelling', updated_at = ${now} WHERE id = ? AND state = 'running'`, [id2]));
1114
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'cancelled', reason = 'parent-ended', updated_at = ${now} WHERE parent_id = ? AND state = 'queued'`, [id2]));
1115
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'cancelling', reason = 'parent-ended', updated_at = ${now} WHERE parent_id = ? AND state = 'running'`, [id2]));
1116
+ const changed = await find(sql, id2, row.scope_key);
1117
+ return changed ? ok(projectRequest(changed)) : missing();
1118
+ });
1119
+ },
1120
+ async reserveWarm(pool) {
1121
+ const environment2 = envById.get(pool.environmentId);
1122
+ if (!environment2 || !pool.minReady || !envKeys.includes(hash(environment2)))
1123
+ return ok(null);
1124
+ const fingerprint = hash({
1125
+ pool: { ...pool },
1126
+ environment: environment2
1127
+ });
1128
+ const key = hash([pool.kind, pool.key]);
1129
+ const usage = columns(environment2.resources);
1130
+ async function canReserve(sql) {
1131
+ const [totalsResult, reservedResult] = await Promise.all([
1132
+ sql.get(`
1133
+ SELECT COUNT(*) AS total, COALESCE(SUM(CASE WHEN state = 'running' AND pool_fingerprint = ?
1134
+ AND warm_state IN ('starting','ready') THEN 1 ELSE 0 END),0) AS warm,
1135
+ COALESCE(MIN(pool_limit), ?) AS ceiling
1136
+ FROM ${EXECUTIONS} WHERE concurrency_key = ? AND ${ACTIVE}`, [fingerprint, pool.maxTotal, key]),
1137
+ sql.get(`
1138
+ SELECT COUNT(*) AS active, COALESCE(SUM(cpu_milli),0) AS cpu, COALESCE(SUM(memory_mib),0) AS memory,
1139
+ COALESCE(SUM(pids),0) AS pids, COALESCE(SUM(scratch_mib),0) AS scratch
1140
+ FROM ${EXECUTIONS} WHERE owner_node = ? AND capacity_class = 'tool' AND ${ACTIVE}`, [owner.nodeId])
1141
+ ]);
1142
+ const totals = data(totalsResult);
1143
+ const reserved = data(reservedResult);
1144
+ if (!totals || !reserved || Number(totals.warm) >= pool.minReady || Number(totals.total) >= Math.min(pool.maxTotal, Number(totals.ceiling)) || Number(reserved.active) >= config.toolMaxRunning) {
1145
+ return false;
1146
+ }
1147
+ const capacity = columns(config.toolCapacity);
1148
+ const occupied = [
1149
+ Number(reserved.cpu),
1150
+ Number(reserved.memory),
1151
+ Number(reserved.pids),
1152
+ Number(reserved.scratch)
1153
+ ];
1154
+ return !usage.some((amount, index) => amount + occupied[index] > capacity[index]);
1155
+ }
1156
+ try {
1157
+ if (!await canReserve(reldb.sql))
1158
+ return ok(null);
1159
+ } catch {
1160
+ return unavailable2();
1161
+ }
1162
+ return serialized(async (sql) => {
1163
+ if (!await canReserve(sql))
1164
+ return ok(null);
1165
+ const id2 = hash([
1166
+ owner.generation,
1167
+ randomUUID()
1168
+ ]);
1169
+ const request = {
1170
+ scope: { tenantId: "@runner", userId: "@warm" },
1171
+ input: null,
1172
+ environment: environment2,
1173
+ resources: environment2.resources,
1174
+ runTimeoutMs: 864e5
1175
+ };
1176
+ data(await sql.execute(`INSERT INTO ${EXECUTIONS}
1177
+ (id, scope_key, request_key, kind, request_json, environment_key, state, owner_node, owner_generation,
1178
+ leased_until, queue_until, run_until, event_sequence, cpu_milli, memory_mib, pids, scratch_mib,
1179
+ created_at, updated_at, capacity_class, concurrency_key, concurrency_limit, pool_fingerprint, pool_limit, warm_state)
1180
+ VALUES (?, ?, ?, ?, ?, ?, 'running', ?, ?, ${now} + ?, 0, ${now} + ?, 0, ?, ?, ?, ?,
1181
+ ${now}, ${now}, 'tool', ?, ?, ?, ?, 'starting')`, [
1182
+ id2,
1183
+ hash(["@runner", "@warm"]),
1184
+ id2,
1185
+ pool.kind,
1186
+ JSON.stringify(request),
1187
+ hash(environment2),
1188
+ owner.nodeId,
1189
+ owner.generation,
1190
+ config.leaseMs,
1191
+ request.runTimeoutMs,
1192
+ ...usage,
1193
+ key,
1194
+ pool.maxRunning,
1195
+ fingerprint,
1196
+ pool.maxTotal
1197
+ ]));
1198
+ return ok({ id: id2, kind: pool.kind, request });
1199
+ });
1200
+ },
1201
+ async markWarmReady(id2) {
1202
+ const result = await reldb.sql.execute(`UPDATE ${EXECUTIONS} SET warm_state = 'ready', updated_at = ${now}
1203
+ WHERE id = ? AND owner_generation = ? AND state = 'running' AND warm_state = 'starting'
1204
+ AND leased_until > ${now} AND container_json IS NOT NULL`, [id2, owner.generation]);
1205
+ return result.success && result.data.changes === 1 ? ok(void 0) : lost();
1206
+ },
1207
+ async claim(kinds, category, pools = [], warmIds = []) {
1208
+ if (!kinds.length)
1209
+ return ok(null);
1210
+ const capacityClass = category === "tool" ? "tool" : "default";
1211
+ const parentFilter = category === "control" ? "IS NOT NULL" : "IS NULL";
1212
+ const queued = await reldb.sql.get(`
1213
+ SELECT id FROM ${EXECUTIONS} WHERE state = 'queued' AND capacity_class = ? AND parent_id ${parentFilter}
1214
+ AND queue_until > ${now} AND kind IN (${kinds.map(() => "?").join(",")})
1215
+ AND (pinned_node IS NULL OR pinned_node = ?) AND (pinned_generation IS NULL OR pinned_generation = ?) LIMIT 1`, [
1216
+ capacityClass,
1217
+ ...kinds,
1218
+ owner.nodeId,
1219
+ owner.generation
1220
+ ]);
1221
+ if (!queued.success)
1222
+ return unavailable2();
1223
+ if (!queued.data)
1224
+ return ok(null);
1225
+ async function eligible(sql, includeRequest) {
1226
+ const maxRunning = category === "tool" ? config.toolMaxRunning : category === "control" ? config.maxControlRunning : config.maxRunning;
1227
+ const reserved = data(await sql.get(
1228
+ `SELECT COUNT(*) AS active, COALESCE(SUM(cpu_milli),0) AS cpu, COALESCE(SUM(memory_mib),0) AS memory,
1229
+ COALESCE(SUM(pids),0) AS pids, COALESCE(SUM(scratch_mib),0) AS scratch
1230
+ FROM ${EXECUTIONS} WHERE owner_node = ? AND capacity_class = ? AND ${ACTIVE} AND parent_id ${parentFilter}`,
1231
+ [owner.nodeId, capacityClass]
1232
+ ));
1233
+ if (!reserved)
1234
+ return null;
1235
+ const fingerprints = category === "tool" ? pools.flatMap((pool) => {
1236
+ const environment2 = envById.get(pool.environmentId);
1237
+ return environment2 ? [
1238
+ hash({
1239
+ pool: { ...pool },
1240
+ environment: environment2
1241
+ })
1242
+ ] : [];
1243
+ }) : [];
1244
+ const eligiblePool = fingerprints.length ? ` OR q.pool_fingerprint IN (${fingerprints.map(() => "?").join(",")})` : "";
1245
+ const ready = category === "tool" && warmIds.length ? `EXISTS (SELECT 1 FROM ${EXECUTIONS} w WHERE w.id IN (${warmIds.map(() => "?").join(",")})
1246
+ AND w.pool_fingerprint = q.pool_fingerprint AND w.state = 'running' AND w.warm_state = 'ready' AND w.leased_until > ${now})` : "1 = 0";
1247
+ const canStartCold = Number(reserved.active) < maxRunning;
1248
+ const eligibleEnvironment = envKeys.length ? ` OR q.environment_key IN (${envKeys.map(() => "?").join(",")})` : "";
1249
+ const [cpu, memory, pids, scratch] = columns(category === "tool" ? config.toolCapacity : config.capacity);
1250
+ return data(await sql.get(`
1251
+ SELECT q.id${includeRequest ? ", q.kind, q.request_json, q.parent_id, q.pool_fingerprint" : ""} FROM ${EXECUTIONS} q
1252
+ WHERE q.state = 'queued' AND q.capacity_class = ? AND q.queue_until > ${now} AND q.parent_id ${parentFilter}
1253
+ AND (q.pinned_node IS NULL OR q.pinned_node = ?) AND (q.pinned_generation IS NULL OR q.pinned_generation = ?)
1254
+ AND q.kind IN (${kinds.map(() => "?").join(",")}) AND (q.environment_key IS NULL${eligibleEnvironment})
1255
+ AND (q.pool_fingerprint IS NULL${eligiblePool})
1256
+ AND ((${ready}) OR (? = 1 AND q.cpu_milli <= ? AND q.memory_mib <= ? AND q.pids <= ? AND q.scratch_mib <= ?
1257
+ AND (q.pool_fingerprint IS NULL OR (
1258
+ NOT EXISTS (SELECT 1 FROM ${EXECUTIONS} w WHERE w.pool_fingerprint = q.pool_fingerprint
1259
+ AND w.state = 'running' AND w.warm_state IN ('starting','ready') AND w.leased_until > ${now}
1260
+ AND (q.pinned_node IS NULL OR q.pinned_node = w.owner_node))
1261
+ AND (SELECT COUNT(*) FROM ${EXECUTIONS} a WHERE a.concurrency_key = q.concurrency_key AND ${ACTIVE})
1262
+ < ${reldb.config?.type === "sqlite" ? "MIN" : "LEAST"}(q.pool_limit, COALESCE((SELECT MIN(a.pool_limit) FROM ${EXECUTIONS} a
1263
+ WHERE a.concurrency_key = q.concurrency_key AND ${ACTIVE}), q.pool_limit))))))
1264
+ AND (q.parent_id IS NULL OR EXISTS (SELECT 1 FROM ${EXECUTIONS} p
1265
+ WHERE p.id = q.parent_id AND p.state = 'running' AND p.leased_until > ${now} AND p.run_until > ${now}))
1266
+ AND (SELECT COUNT(*) FROM ${EXECUTIONS} a WHERE a.scope_key = q.scope_key AND a.warm_state IS NULL AND ${ACTIVE}
1267
+ AND a.capacity_class = q.capacity_class AND a.parent_id ${parentFilter}) < ?
1268
+ AND (q.concurrency_key IS NULL OR (
1269
+ (SELECT COUNT(*) FROM ${EXECUTIONS} a WHERE a.concurrency_key = q.concurrency_key AND a.warm_state IS NULL AND ${ACTIVE}) < q.concurrency_limit
1270
+ AND (SELECT COUNT(*) FROM ${EXECUTIONS} a WHERE a.concurrency_key = q.concurrency_key AND a.warm_state IS NULL AND ${ACTIVE})
1271
+ < COALESCE((SELECT MIN(a.concurrency_limit) FROM ${EXECUTIONS} a WHERE a.concurrency_key = q.concurrency_key AND ${ACTIVE}), q.concurrency_limit)))
1272
+ ORDER BY (SELECT COUNT(*) FROM ${EXECUTIONS} a WHERE a.scope_key = q.scope_key
1273
+ AND ${ACTIVE} AND a.capacity_class = q.capacity_class AND a.parent_id ${parentFilter}) ASC, q.created_at ASC, q.id ASC
1274
+ LIMIT 1
1275
+ `, [
1276
+ capacityClass,
1277
+ owner.nodeId,
1278
+ owner.generation,
1279
+ ...kinds,
1280
+ ...envKeys,
1281
+ ...fingerprints,
1282
+ ...category === "tool" ? warmIds : [],
1283
+ Number(canStartCold),
1284
+ cpu - Number(reserved.cpu),
1285
+ memory - Number(reserved.memory),
1286
+ pids - Number(reserved.pids),
1287
+ scratch - Number(reserved.scratch),
1288
+ config.maxRunningPerScope
1289
+ ]));
1290
+ }
1291
+ try {
1292
+ if (!await eligible(reldb.sql, false))
1293
+ return ok(null);
1294
+ } catch {
1295
+ return unavailable2();
1296
+ }
1297
+ return serialized(async (sql) => {
1298
+ const row = await eligible(sql, true);
1299
+ if (!row)
1300
+ return ok(null);
1301
+ const request = databaseValue(row.request_json);
1302
+ const standby = row.pool_fingerprint && warmIds.length ? data(await sql.get(`
1303
+ SELECT * FROM ${EXECUTIONS} WHERE id IN (${warmIds.map(() => "?").join(",")}) AND pool_fingerprint = ?
1304
+ AND state = 'running' AND warm_state = 'ready' AND leased_until > ${now} ORDER BY created_at, id LIMIT 1`, [...warmIds, row.pool_fingerprint])) : null;
1305
+ const changed = data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'running', owner_node = ?, owner_generation = ?,
1306
+ container_json = ${resultValue}, owner_process_json = ${resultValue}, leased_until = ${now} + ?, run_until = ${now} + ?, updated_at = ${now}
1307
+ WHERE id = ? AND state = 'queued' AND queue_until > ${now}`, [
1308
+ owner.nodeId,
1309
+ owner.generation,
1310
+ standby?.container_json ? JSON.stringify(databaseValue(standby.container_json)) : null,
1311
+ request.environment ? null : processJson,
1312
+ config.leaseMs,
1313
+ request.runTimeoutMs,
1314
+ row.id
1315
+ ]));
1316
+ if (changed.changes === 1 && standby) {
1317
+ const transferred = data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'succeeded', warm_state = 'transferred', updated_at = ${now}
1318
+ WHERE id = ? AND owner_generation = ? AND state = 'running' AND warm_state = 'ready' AND leased_until > ${now}`, [standby.id, owner.generation]));
1319
+ if (transferred.changes !== 1)
1320
+ throw new Error("Warm container reservation expired");
1321
+ }
1322
+ return ok(changed.changes === 1 ? {
1323
+ id: row.id,
1324
+ kind: row.kind,
1325
+ parentId: row.parent_id ?? void 0,
1326
+ warmId: standby?.id,
1327
+ request
1328
+ } : null);
1329
+ });
1330
+ },
1331
+ async cancelOwned(id2) {
1332
+ const result = await reldb.sql.execute(`UPDATE ${EXECUTIONS} SET state = 'cancelling', updated_at = ${now}
1333
+ WHERE id = ? AND owner_generation = ? AND state = 'running' AND leased_until > ${now}`, [id2, owner.generation]);
1334
+ return result.success ? ok(void 0) : unavailable2();
1335
+ },
1336
+ async recordContainer(id2, container) {
1337
+ const result = await reldb.sql.execute(`UPDATE ${EXECUTIONS} SET container_json = ${resultValue}
1338
+ WHERE id = ? AND owner_generation = ? AND state = 'running' AND container_json IS NULL
1339
+ AND leased_until > ${now} AND run_until > ${now}`, [
1340
+ JSON.stringify(container),
1341
+ id2,
1342
+ owner.generation
1343
+ ]);
1344
+ return result.success && result.data.changes === 1 ? ok(void 0) : lost();
1345
+ },
1346
+ async reconciliation(scope, id2) {
1347
+ try {
1348
+ const row = await find(reldb.sql, id2, hash([scope.tenantId, scope.userId]));
1349
+ if (!row)
1350
+ return missing();
1351
+ if (row.state !== "unknown" || row.owner_node !== owner.nodeId || row.reconciled_at !== null || !row.owner_generation) {
1352
+ return conflict();
1353
+ }
1354
+ if (row.container_json) {
1355
+ const container = databaseValue(row.container_json);
1356
+ return container.engine === config.engine ? ok({ container, generation: row.owner_generation }) : conflict();
1357
+ }
1358
+ if (row.environment_key !== null || !row.owner_process_json || !await parentSettled(reldb.sql, row))
1359
+ return conflict();
1360
+ return ok({
1361
+ container: void 0,
1362
+ process: databaseValue(row.owner_process_json),
1363
+ generation: row.owner_generation
1364
+ });
1365
+ } catch {
1366
+ return unavailable2();
1367
+ }
1368
+ },
1369
+ async reconcile(scope, id2, generation, review) {
1370
+ return serialized(async (sql) => {
1371
+ const scopeKey = hash([scope.tenantId, scope.userId]);
1372
+ if (review.hostResourcesStopped) {
1373
+ const row2 = await find(sql, id2, scopeKey);
1374
+ if (!row2 || !await parentSettled(sql, row2))
1375
+ return conflict();
1376
+ }
1377
+ const proof = review.hostResourcesStopped ? "container_json IS NULL AND environment_key IS NULL AND owner_process_json IS NOT NULL" : "container_json IS NOT NULL";
1378
+ const result = data(await sql.execute(`UPDATE ${EXECUTIONS}
1379
+ SET reconciled_at = ${now}, reconciliation_json = ${resultValue}, reuse_key = NULL, updated_at = ${now}
1380
+ WHERE id = ? AND scope_key = ? AND owner_node = ? AND owner_generation = ? AND state = 'unknown'
1381
+ AND reconciled_at IS NULL AND ${proof}`, [
1382
+ JSON.stringify(review),
1383
+ id2,
1384
+ scopeKey,
1385
+ owner.nodeId,
1386
+ generation
1387
+ ]));
1388
+ if (result.changes !== 1)
1389
+ return conflict();
1390
+ const row = await find(sql, id2, scopeKey);
1391
+ return row ? ok(projectRequest(row)) : missing();
1392
+ });
1393
+ },
1394
+ async renew(id2) {
1395
+ const renewed = await reldb.sql.execute(`UPDATE ${EXECUTIONS} SET leased_until = ${now} + ?, updated_at = ${now}
1396
+ WHERE id = ? AND owner_generation = ? AND state IN ('running', 'cancelling') AND leased_until > ${now}`, [config.leaseMs, id2, owner.generation]);
1397
+ if (!renewed.success)
1398
+ return unavailable2();
1399
+ if (renewed.data.changes !== 1)
1400
+ return lost();
1401
+ const row = await reldb.sql.get(
1402
+ `SELECT state, parent_id FROM ${EXECUTIONS} WHERE id = ? AND owner_generation = ? AND leased_until > ${now}`,
1403
+ [id2, owner.generation]
1404
+ );
1405
+ if (!row.success)
1406
+ return unavailable2();
1407
+ if (!row.data)
1408
+ return lost();
1409
+ if (row.data.parent_id) {
1410
+ const parent = await reldb.sql.get(`SELECT id FROM ${EXECUTIONS} WHERE id = ?
1411
+ AND owner_generation = ? AND state = 'running' AND leased_until > ${now} AND run_until > ${now}`, [row.data.parent_id, owner.generation]);
1412
+ if (!parent.success)
1413
+ return unavailable2();
1414
+ if (!parent.data) {
1415
+ const cancelled = await this.cancelOwned(id2);
1416
+ return cancelled.success ? ok("cancelling") : cancelled;
1417
+ }
1418
+ }
1419
+ return ok(row.data.state);
1420
+ },
1421
+ async emit(id2, value) {
1422
+ const encoded = JSON.stringify({ value });
1423
+ const bytes = Buffer.byteLength(encoded);
1424
+ const result = await reldb.tx.wrap(async (sql) => {
1425
+ const changed = data(await sql.execute(`UPDATE ${EXECUTIONS} SET event_sequence = event_sequence + 1, event_bytes = event_bytes + ?
1426
+ WHERE id = ? AND owner_generation = ? AND state = 'running' AND leased_until > ${now}
1427
+ AND run_until > ${now} AND event_sequence < ? AND event_bytes <= ?`, [
1428
+ bytes,
1429
+ id2,
1430
+ owner.generation,
1431
+ config.maxEvents,
1432
+ MAX_EVENT_BYTES - bytes
1433
+ ]));
1434
+ if (changed.changes !== 1)
1435
+ return lost();
1436
+ const row = data(await sql.get(`SELECT event_sequence FROM ${EXECUTIONS} WHERE id = ?`, [id2]));
1437
+ const sequence = Number(row?.event_sequence);
1438
+ const published = data(await sql.execute(`INSERT INTO ${EVENTS} (id, execution_id, sequence_number, event_json, created_at)
1439
+ SELECT ?, ?, ?, ${resultValue}, ${now} FROM ${EXECUTIONS} q LEFT JOIN ${EXECUTIONS} p ON p.id = q.parent_id
1440
+ WHERE q.id = ? AND q.leased_until > ${now} AND q.run_until > ${now}
1441
+ AND (q.parent_id IS NULL OR (p.state = 'running' AND p.owner_generation = q.owner_generation
1442
+ AND p.leased_until > ${now} AND p.run_until > ${now}))`, [
1443
+ hash([id2, sequence]),
1444
+ id2,
1445
+ sequence,
1446
+ encoded,
1447
+ id2
1448
+ ]));
1449
+ if (published.changes !== 1)
1450
+ throw new Error("Event owner lost");
1451
+ return ok(void 0);
1452
+ });
1453
+ return result.success ? result.data : unavailable2();
1454
+ },
1455
+ async events(scope, id2, after, limit) {
1456
+ const owner2 = await reldb.sql.get(`SELECT id FROM ${EXECUTIONS} WHERE id = ? AND scope_key = ?`, [
1457
+ id2,
1458
+ hash([scope.tenantId, scope.userId])
1459
+ ]);
1460
+ if (!owner2.success)
1461
+ return unavailable2();
1462
+ if (!owner2.data)
1463
+ return missing();
1464
+ const page = await reldb.sql.query(
1465
+ `SELECT sequence_number, event_json, created_at FROM ${EVENTS}
1466
+ WHERE execution_id = ? AND sequence_number > ? ORDER BY sequence_number ASC LIMIT ?`,
1467
+ [id2, after, limit]
1468
+ );
1469
+ if (!page.success)
1470
+ return unavailable2();
1471
+ try {
1472
+ return ok(page.data.map((row) => ({
1473
+ sequence: Number(row.sequence_number),
1474
+ data: databaseValue(row.event_json).value,
1475
+ createdAt: Number(row.created_at)
1476
+ })));
1477
+ } catch {
1478
+ return unavailable2();
1479
+ }
1480
+ },
1481
+ async finish(id2, output, stopped) {
1482
+ return serialized(async (sql) => {
1483
+ if (!stopped) {
1484
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'unknown', reason = 'cleanup-unconfirmed', updated_at = ${now}
1485
+ WHERE id = ? AND owner_generation = ? AND state IN ('running', 'cancelling') AND leased_until > ${now}`, [id2, owner.generation]));
1486
+ return lost();
1487
+ }
1488
+ const child = data(await sql.get(
1489
+ `SELECT parent_id FROM ${EXECUTIONS} WHERE id = ? AND owner_generation = ?`,
1490
+ [id2, owner.generation]
1491
+ ));
1492
+ if (!child)
1493
+ return lost();
1494
+ const parentLock = reldb.config?.type === "sqlite" ? "" : " FOR UPDATE";
1495
+ const parent = child.parent_id ? data(await sql.get(
1496
+ `SELECT * FROM ${EXECUTIONS} WHERE id = ?${parentLock}`,
1497
+ [child.parent_id]
1498
+ )) : null;
1499
+ const parentUntil = child.parent_id ? parent?.state === "running" && parent.owner_generation === owner.generation ? Math.min(Number(parent.leased_until), Number(parent.run_until)) : 0 : Number.MAX_SAFE_INTEGER;
1500
+ const changed = data(await sql.execute(`UPDATE ${EXECUTIONS}
1501
+ SET result_json = CASE WHEN state = 'running' AND run_until > ${now} AND ? > ${now} THEN ${resultValue} ELSE NULL END,
1502
+ reason = CASE WHEN reason = 'run-timeout' OR run_until <= ${now} THEN 'run-timeout'
1503
+ WHEN ? <= ${now} THEN 'parent-ended' WHEN state = 'cancelling' THEN reason ELSE ? END,
1504
+ state = CASE WHEN reason = 'run-timeout' OR run_until <= ${now} THEN 'failed'
1505
+ WHEN state = 'cancelling' OR ? <= ${now} THEN 'cancelled' ELSE ? END,
1506
+ reuse_key = NULL, updated_at = ${now}
1507
+ WHERE id = ? AND owner_generation = ? AND state IN ('running', 'cancelling') AND leased_until > ${now}`, [
1508
+ parentUntil,
1509
+ output === void 0 ? null : JSON.stringify({ value: output }),
1510
+ parentUntil,
1511
+ output === void 0 ? "handler-failed" : null,
1512
+ parentUntil,
1513
+ output === void 0 ? "failed" : "succeeded",
1514
+ id2,
1515
+ owner.generation
1516
+ ]));
1517
+ if (changed.changes !== 1)
1518
+ return lost();
1519
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'cancelled', reason = 'parent-ended', updated_at = ${now} WHERE parent_id = ? AND state = 'queued'`, [id2]));
1520
+ data(await sql.execute(`UPDATE ${EXECUTIONS} SET state = 'cancelling', reason = 'parent-ended', updated_at = ${now} WHERE parent_id = ? AND state = 'running'`, [id2]));
1521
+ return ok(void 0);
1522
+ });
1523
+ }
1524
+ };
1525
+ }
1526
+
1527
+ // src/runner-main.ts
1528
+ var logger = core.logger.child({ module: "runner" });
1529
+ var inactive = core.module.createNotInitializedKit(HaiRunnerError.NOT_INITIALIZED, () => runnerM("runner_notInitialized")).result;
1530
+ var MAX_PAYLOAD_BYTES = 512 * 1024;
1531
+ function invalid2() {
1532
+ return err(HaiRunnerError.INVALID, runnerM("runner_invalid"));
1533
+ }
1534
+ function cleanupUnconfirmed() {
1535
+ return err(HaiRunnerError.CLEANUP_UNCONFIRMED, runnerM("runner_cleanupUnconfirmed"));
1536
+ }
1537
+ function notFound() {
1538
+ return err(HaiRunnerError.NOT_FOUND, runnerM("runner_notFound"));
1539
+ }
1540
+ function validAddress(scope, id2) {
1541
+ return !!scope && typeof scope.tenantId === "string" && typeof scope.userId === "string" && scope.tenantId.length > 0 && scope.tenantId.length <= 200 && scope.userId.length > 0 && scope.userId.length <= 200 && /^[a-f\d]{64}$/u.test(id2);
1542
+ }
1543
+ function bounded(input) {
1544
+ try {
1545
+ return Buffer.byteLength(JSON.stringify(input)) <= MAX_PAYLOAD_BYTES;
1546
+ } catch {
1547
+ return false;
1548
+ }
1549
+ }
1550
+ function createRunner() {
1551
+ let config;
1552
+ let store;
1553
+ let owner;
1554
+ let initializing = false;
1555
+ let closing;
1556
+ let closingRequested = false;
1557
+ let timer;
1558
+ let tick;
1559
+ let sweepAt = 0;
1560
+ let pruneAt = 0;
1561
+ let unconfirmedCleanup = false;
1562
+ const running = /* @__PURE__ */ new Map();
1563
+ const handlers = /* @__PURE__ */ new Map();
1564
+ const pools = /* @__PURE__ */ new Map();
1565
+ const warmReservations = /* @__PURE__ */ new Map();
1566
+ const standby = /* @__PURE__ */ new Map();
1567
+ let warmAt = 0;
1568
+ const poolKey = (kind, key) => `${kind}:${key}`;
1569
+ function prewarm(claim, pool, current, settings) {
1570
+ const abort = new AbortController();
1571
+ const environment2 = claim.request.environment;
1572
+ warmReservations.set(claim.id, pool);
1573
+ const done = (async () => {
1574
+ const launched = await prepareWarmContainer(settings.engine, environment2, abort.signal, (identity) => current.recordContainer(claim.id, identity));
1575
+ if (!launched.success) {
1576
+ const stopped = launched.error.code !== HaiRunnerError.CLEANUP_UNCONFIRMED.code;
1577
+ await current.finish(claim.id, void 0, stopped);
1578
+ if (!stopped)
1579
+ unconfirmedCleanup = true;
1580
+ return stopped;
1581
+ }
1582
+ const handle = launched.data;
1583
+ handle.stderr.resume();
1584
+ let transferred = false;
1585
+ const released = new Promise((resolve) => {
1586
+ standby.set(claim.id, {
1587
+ handle,
1588
+ pool,
1589
+ transferred: () => {
1590
+ transferred = true;
1591
+ resolve();
1592
+ }
1593
+ });
1594
+ abort.signal.addEventListener("abort", () => resolve(), { once: true });
1595
+ if (abort.signal.aborted)
1596
+ resolve();
1597
+ });
1598
+ const ready = await current.markWarmReady(claim.id);
1599
+ if (ready.success && !abort.signal.aborted) {
1600
+ await Promise.race([
1601
+ released,
1602
+ handle.wait()
1603
+ ]);
1604
+ }
1605
+ standby.delete(claim.id);
1606
+ if (transferred)
1607
+ return true;
1608
+ const closed = await handle.close();
1609
+ await current.finish(claim.id, void 0, closed.success);
1610
+ if (!closed.success)
1611
+ unconfirmedCleanup = true;
1612
+ return closed.success;
1613
+ })().catch(() => {
1614
+ unconfirmedCleanup = true;
1615
+ logger.warn("Warm container cleanup could not be confirmed", { executionId: claim.id });
1616
+ return false;
1617
+ }).finally(() => {
1618
+ running.delete(claim.id);
1619
+ warmReservations.delete(claim.id);
1620
+ });
1621
+ running.set(claim.id, { abort, done, renewAt: performance.now() + settings.pollMs });
1622
+ }
1623
+ function launch(claim, handler, current, settings, holder, warmed) {
1624
+ const abort = new AbortController();
1625
+ let process3;
1626
+ let uncertain2 = false;
1627
+ const closeProcess = async () => {
1628
+ const started = await process3;
1629
+ if (started?.success)
1630
+ return (await started.data.close()).success;
1631
+ return warmed ? (await warmed.close()).success : !uncertain2;
1632
+ };
1633
+ async function start(options) {
1634
+ const { environment: environment2, resources: resources2 } = claim.request;
1635
+ if (abort.signal.aborted || process3 || !environment2 || !resources2)
1636
+ return invalid2();
1637
+ process3 = (async () => {
1638
+ const renewed = await current.renew(claim.id);
1639
+ if (!renewed.success)
1640
+ return renewed;
1641
+ if (renewed.data !== "running" || abort.signal.aborted)
1642
+ return err(HaiRunnerError.LEASE_LOST, runnerM("runner_leaseLost"));
1643
+ const started = warmed ? await warmed.activate(options, abort.signal, async () => {
1644
+ const lease = await current.renew(claim.id);
1645
+ return lease.success && lease.data === "running";
1646
+ }) : await startContainer(settings.engine, environment2, resources2, options, abort.signal, (identity) => current.recordContainer(claim.id, identity));
1647
+ if (!started.success && started.error.code === HaiRunnerError.CLEANUP_UNCONFIRMED.code)
1648
+ uncertain2 = true;
1649
+ return started;
1650
+ })();
1651
+ return process3;
1652
+ }
1653
+ async function execute2() {
1654
+ const context = {
1655
+ id: claim.id,
1656
+ parentId: claim.parentId,
1657
+ scope: claim.request.scope,
1658
+ owner: holder,
1659
+ signal: abort.signal,
1660
+ start,
1661
+ emit: async (value) => {
1662
+ if (abort.signal.aborted || !bounded(value))
1663
+ return invalid2();
1664
+ try {
1665
+ const parsed = json.safeParse(value);
1666
+ return parsed.success ? await current.emit(claim.id, parsed.data) : invalid2();
1667
+ } catch {
1668
+ return invalid2();
1669
+ }
1670
+ }
1671
+ };
1672
+ const stop = () => {
1673
+ void closeProcess();
1674
+ };
1675
+ abort.signal.addEventListener("abort", stop, { once: true });
1676
+ let output;
1677
+ try {
1678
+ const lease = await current.renew(claim.id);
1679
+ if (lease.success && lease.data === "running" && !abort.signal.aborted) {
1680
+ const value = await handler(claim.request.input, context);
1681
+ if (!bounded(value))
1682
+ throw new Error("Runner handler output exceeded its bound");
1683
+ const parsed = json.safeParse(value);
1684
+ if (parsed.success)
1685
+ output = parsed.data;
1686
+ }
1687
+ } catch {
1688
+ logger.warn("Managed execution handler failed", { executionId: claim.id, kind: claim.kind });
1689
+ } finally {
1690
+ abort.signal.removeEventListener("abort", stop);
1691
+ }
1692
+ const stopped = await closeProcess();
1693
+ if (!stopped)
1694
+ unconfirmedCleanup = true;
1695
+ const result = await current.finish(claim.id, output, stopped);
1696
+ if (!result.success)
1697
+ logger.warn("Managed execution final state could not be committed", { executionId: claim.id, code: result.error.code });
1698
+ return stopped;
1699
+ }
1700
+ const done = Promise.resolve().then(execute2).catch(() => {
1701
+ unconfirmedCleanup = true;
1702
+ logger.warn("Managed execution failed to finish", { executionId: claim.id });
1703
+ return false;
1704
+ }).finally(() => running.delete(claim.id));
1705
+ running.set(claim.id, { abort, done, renewAt: performance.now() + settings.pollMs });
1706
+ }
1707
+ function poll() {
1708
+ if (tick || !store || !config || !owner)
1709
+ return;
1710
+ const current = store;
1711
+ const settings = config;
1712
+ const holder = owner;
1713
+ tick = (async () => {
1714
+ await Promise.all([...running].map(async ([id2, execution]) => {
1715
+ if (execution.renewAt > performance.now())
1716
+ return;
1717
+ const result = await current.renew(id2);
1718
+ execution.renewAt = performance.now() + (result.success && result.data === "running" ? Math.min(250, settings.leaseMs / 4) : settings.pollMs);
1719
+ if (!result.success || result.data !== "running")
1720
+ execution.abort.abort();
1721
+ }));
1722
+ if (!closingRequested && sweepAt <= performance.now()) {
1723
+ const maintenance = await current.sweep();
1724
+ if (!maintenance.success)
1725
+ logger.warn("Managed execution maintenance failed", { code: maintenance.error.code });
1726
+ sweepAt = performance.now() + Math.min(250, settings.leaseMs / 4);
1727
+ }
1728
+ if (closingRequested)
1729
+ return;
1730
+ if (pruneAt <= performance.now()) {
1731
+ const pruned = await current.prune();
1732
+ if (!pruned.success)
1733
+ logger.warn("Managed execution retention failed", { code: pruned.error.code });
1734
+ pruneAt = performance.now() + (pruned.success && pruned.data ? 250 : 1e3);
1735
+ }
1736
+ const kinds = [
1737
+ ...handlers.keys()
1738
+ ];
1739
+ if (!kinds.length)
1740
+ return;
1741
+ const configuredPools = [
1742
+ ...pools.values()
1743
+ ].filter((pool) => handlers.has(pool.kind));
1744
+ for (const category of ["control", "tool", "default"]) {
1745
+ const available = [...standby].filter(([, candidate]) => pools.get(poolKey(candidate.pool.kind, candidate.pool.key)) === candidate.pool).map(([id2]) => id2);
1746
+ const claimed = await current.claim(kinds, category, configuredPools, available);
1747
+ if (!claimed.success) {
1748
+ logger.warn("Managed execution claim failed", { code: claimed.error.code });
1749
+ continue;
1750
+ }
1751
+ if (!claimed.data)
1752
+ continue;
1753
+ const handler = handlers.get(claimed.data.kind);
1754
+ const warmed = claimed.data.warmId ? standby.get(claimed.data.warmId) : void 0;
1755
+ warmed?.transferred();
1756
+ if (handler && !closingRequested && (!claimed.data.warmId || warmed)) {
1757
+ launch(claimed.data, handler, current, settings, holder, warmed?.handle);
1758
+ } else {
1759
+ const stopped = warmed ? (await warmed.handle.close()).success : !claimed.data.warmId;
1760
+ await current.finish(claimed.data.id, void 0, stopped);
1761
+ }
1762
+ }
1763
+ if (!closingRequested && performance.now() >= warmAt) {
1764
+ warmAt = performance.now() + Math.max(250, settings.pollMs);
1765
+ for (const pool of configuredPools) {
1766
+ const reserved = await current.reserveWarm(pool);
1767
+ if (reserved.success && reserved.data)
1768
+ prewarm(reserved.data, pool, current, settings);
1769
+ }
1770
+ }
1771
+ })().catch(() => {
1772
+ for (const execution of running.values())
1773
+ execution.abort.abort();
1774
+ logger.warn("Managed execution polling failed");
1775
+ }).finally(() => {
1776
+ tick = void 0;
1777
+ });
1778
+ }
1779
+ async function init(input) {
1780
+ if (initializing || config || closing)
1781
+ return err(HaiRunnerError.ALREADY_INITIALIZED, runnerM("runner_alreadyInitialized"));
1782
+ const parsed = RunnerConfigSchema.safeParse(input);
1783
+ if (!parsed.success)
1784
+ return invalid2();
1785
+ initializing = true;
1786
+ try {
1787
+ const ready = await readyStore();
1788
+ if (!ready.success)
1789
+ return ready;
1790
+ owner = {
1791
+ nodeId: parsed.data.nodeId,
1792
+ generation: randomUUID()
1793
+ };
1794
+ config = parsed.data;
1795
+ store = makeStore(config, owner, config.executor ? await readRunnerProcess() : void 0);
1796
+ unconfirmedCleanup = false;
1797
+ sweepAt = 0;
1798
+ pruneAt = 0;
1799
+ warmAt = 0;
1800
+ if (config.executor) {
1801
+ timer = setInterval(poll, config.pollMs);
1802
+ timer.unref();
1803
+ }
1804
+ return ok(void 0);
1805
+ } finally {
1806
+ initializing = false;
1807
+ }
1808
+ }
1809
+ function close() {
1810
+ if (closing)
1811
+ return closing;
1812
+ if (initializing)
1813
+ return Promise.resolve(err(HaiRunnerError.ALREADY_INITIALIZED, runnerM("runner_alreadyInitialized")));
1814
+ if (!config)
1815
+ return Promise.resolve(ok(void 0));
1816
+ closingRequested = true;
1817
+ closing = (async () => {
1818
+ await tick;
1819
+ await Promise.all([...running].map(async ([id2, execution]) => {
1820
+ const requested = await store?.cancelOwned(id2);
1821
+ if (!requested?.success)
1822
+ unconfirmedCleanup = true;
1823
+ execution.abort.abort();
1824
+ }));
1825
+ let deadline;
1826
+ try {
1827
+ const drained = await Promise.race([
1828
+ Promise.all([
1829
+ ...running.values()
1830
+ ].map((execution) => execution.done)).then(() => true),
1831
+ new Promise((resolve) => {
1832
+ deadline = setTimeout(resolve, config.closeTimeoutMs, false);
1833
+ })
1834
+ ]);
1835
+ if (!drained)
1836
+ return cleanupUnconfirmed();
1837
+ clearInterval(timer);
1838
+ handlers.clear();
1839
+ pools.clear();
1840
+ config = void 0;
1841
+ owner = void 0;
1842
+ store = void 0;
1843
+ closingRequested = false;
1844
+ return unconfirmedCleanup ? cleanupUnconfirmed() : ok(void 0);
1845
+ } finally {
1846
+ clearTimeout(deadline);
1847
+ }
1848
+ })().finally(() => {
1849
+ closing = void 0;
1850
+ });
1851
+ return closing;
1852
+ }
1853
+ return {
1854
+ init,
1855
+ close,
1856
+ get isExecutor() {
1857
+ return config?.executor === true && !closingRequested;
1858
+ },
1859
+ environment(id2) {
1860
+ if (!config || closingRequested)
1861
+ return inactive();
1862
+ const found = config.environments.find((item) => item.id === id2);
1863
+ return found ? ok(structuredClone(found)) : notFound();
1864
+ },
1865
+ register(kind, handler) {
1866
+ if (!config || closingRequested)
1867
+ return inactive();
1868
+ if (!runnerKind.safeParse(kind).success || typeof handler !== "function")
1869
+ return invalid2();
1870
+ if (!config.executor)
1871
+ return ok(() => {
1872
+ });
1873
+ if (handlers.has(kind) || handlers.size >= 100)
1874
+ return err(HaiRunnerError.CONFLICT, runnerM("runner_conflict"));
1875
+ handlers.set(kind, handler);
1876
+ return ok(() => {
1877
+ if (handlers.get(kind) === handler)
1878
+ handlers.delete(kind);
1879
+ });
1880
+ },
1881
+ registerWarmPool(input) {
1882
+ if (!config || closingRequested)
1883
+ return inactive();
1884
+ const parsed = warmPoolSchema.safeParse(input);
1885
+ if (!parsed.success)
1886
+ return invalid2();
1887
+ const pool = parsed.data;
1888
+ const environment2 = config.environments.find((item) => item.id === pool.environmentId);
1889
+ if (!environment2 || environment2.networkPolicy !== "offline" || config.executor && !handlers.has(pool.kind))
1890
+ return invalid2();
1891
+ const key = poolKey(pool.kind, pool.key);
1892
+ if (pools.has(key) || pools.size >= 100)
1893
+ return err(HaiRunnerError.CONFLICT, runnerM("runner_conflict"));
1894
+ pools.set(key, pool);
1895
+ return ok(() => {
1896
+ if (pools.get(key) !== pool)
1897
+ return;
1898
+ pools.delete(key);
1899
+ for (const [id2, candidate] of warmReservations) {
1900
+ if (candidate === pool)
1901
+ running.get(id2)?.abort.abort();
1902
+ }
1903
+ });
1904
+ },
1905
+ async submit(input) {
1906
+ if (!store || closingRequested)
1907
+ return inactive();
1908
+ if (!bounded(input))
1909
+ return invalid2();
1910
+ try {
1911
+ const parsed = submitSchema.safeParse(input);
1912
+ if (!parsed.success)
1913
+ return invalid2();
1914
+ const request = parsed.data;
1915
+ const pool = request.concurrency && pools.get(poolKey(request.kind, request.concurrency.key));
1916
+ if (request.warmPool && (!pool || pool.environmentId !== request.environmentId || pool.maxRunning !== request.concurrency?.limit)) {
1917
+ return invalid2();
1918
+ }
1919
+ if (pool && !request.warmPool)
1920
+ return invalid2();
1921
+ return await store.submit(request, pool);
1922
+ } catch {
1923
+ return invalid2();
1924
+ }
1925
+ },
1926
+ async find(scope, kind, key) {
1927
+ if (!store)
1928
+ return inactive();
1929
+ return validAddress(scope, "0".repeat(64)) && runnerKind.safeParse(kind).success && typeof key === "string" && key.length > 0 && key.length <= 200 ? store.find(scope, kind, key) : invalid2();
1930
+ },
1931
+ async get(scope, id2) {
1932
+ if (!store)
1933
+ return inactive();
1934
+ return validAddress(scope, id2) ? store.get(scope, id2) : invalid2();
1935
+ },
1936
+ async cancel(scope, id2) {
1937
+ if (!store)
1938
+ return inactive();
1939
+ return validAddress(scope, id2) ? store.cancel(scope, id2) : invalid2();
1940
+ },
1941
+ async reconcileUnknown(scope, id2, review) {
1942
+ const current = store;
1943
+ if (!current || !config?.executor || closingRequested)
1944
+ return inactive();
1945
+ if (!validAddress(scope, id2) || !review || typeof review.operatorId !== "string" || !runnerKind.safeParse(review.operatorId).success || typeof review.reason !== "string" || review.hostResourcesStopped !== void 0 && review.hostResourcesStopped !== true || !review.reason.trim() || review.reason.length > 300 || /[\r\n\0]/u.test(review.reason)) {
1946
+ return invalid2();
1947
+ }
1948
+ if (running.has(id2))
1949
+ return cleanupUnconfirmed();
1950
+ const evidence = await current.reconciliation(scope, id2);
1951
+ if (!evidence.success)
1952
+ return evidence;
1953
+ if (evidence.data.container) {
1954
+ const verified = await confirmContainerReleased(evidence.data.container);
1955
+ if (!verified.success)
1956
+ return verified;
1957
+ } else if (review.hostResourcesStopped !== true || !await confirmRunnerProcessReleased(evidence.data.process)) {
1958
+ return cleanupUnconfirmed();
1959
+ }
1960
+ if (current !== store || closingRequested)
1961
+ return inactive();
1962
+ const reconciled = await current.reconcile(scope, id2, evidence.data.generation, {
1963
+ operatorId: review.operatorId,
1964
+ reason: review.reason.trim(),
1965
+ ...!evidence.data.container && { hostResourcesStopped: true }
1966
+ });
1967
+ if (reconciled.success)
1968
+ logger.info("Unknown execution resources reconciled", { executionId: id2, operatorId: review.operatorId });
1969
+ return reconciled;
1970
+ },
1971
+ async events(scope, id2, after = 0, limit = 100) {
1972
+ if (!store)
1973
+ return inactive();
1974
+ return validAddress(scope, id2) && Number.isSafeInteger(after) && after >= 0 && Number.isSafeInteger(limit) && limit >= 1 && limit <= 100 ? store.events(scope, id2, after, limit) : invalid2();
1975
+ }
1976
+ };
1977
+ }
1978
+ var runner = createRunner();
1979
+
1980
+ export { HaiRunnerError, RunnerConfigSchema, createRunner, runner };
1981
+ //# sourceMappingURL=index.js.map
1982
+ //# sourceMappingURL=index.js.map