@coreplane/switchboard 1.235.0 → 1.237.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/assets/Dockerfile +11 -0
- package/dist/assets/config/config.example.yaml +9 -4
- package/dist/assets/deploy/cloudflare-memory/worker.ts +12 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +18 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +290 -118
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +104 -17
- package/dist/assets/package-lock.json +212 -3
- package/dist/assets/package.json +4 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/authz/authorize.ts +25 -6
- package/dist/assets/src/core/authz/policy.ts +9 -0
- package/dist/assets/src/core/authz/types.ts +15 -1
- package/dist/assets/src/core/coordinator/contract.ts +24 -4
- package/dist/assets/src/core/coordinator/driver.ts +36 -4
- package/dist/assets/src/core/runEvents.ts +46 -7
- package/dist/assets/src/core/runFriction.ts +1 -0
- package/dist/assets/src/core/ship/contract.ts +3 -1
- package/dist/assets/src/core/ship/coordinator.ts +236 -30
- package/dist/assets/src/core/trace/types.ts +5 -0
- package/dist/assets/src/execution/residentDepsStore.ts +14 -2
- package/dist/assets/src/execution/residentDisk.ts +21 -10
- package/dist/assets/src/execution/residentDiskBudget.ts +45 -11
- package/dist/assets/src/execution/residentHead.ts +34 -8
- package/dist/assets/src/execution/residentRebind.ts +88 -31
- package/dist/assets/src/execution/sandboxErrors.ts +72 -0
- package/dist/assets/src/execution/sandboxLifecycle.ts +33 -0
- package/dist/assets/src/execution/sandboxStart.ts +90 -0
- package/dist/assets/web/dist/.vite/manifest.json +437 -423
- package/dist/assets/web/dist/assets/AppShell-MYHNxJks.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-D-v8am88.js → CostsPage-DKk9OjB0.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-CvlWP7Eq.js → DeliveryPage-Kz6js49i.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-BzWS4Ca1.js → NotFoundPage-BXLrQQzy.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-Dc23v_8v.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-TR5D1TRR.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-mkUj7WEN.js +9 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-oN9GkVEA.js → RunRoutePage-CQI17__g.js} +3 -3
- package/dist/assets/web/dist/assets/RunsIndexPage-yIP4PlKP.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-DEICQ4FY.js → RunsTabs-XpCqeWu4.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-CdCVyNbh.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DuA8JpLU.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-D14iJMy7.js → StatusDot-DmNHX6am.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-B0Ob5MQ4.js → Tooltip-qVnJTgSt.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-BcTeH7m2.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Cw130Zyi.js → angular-html-DYtV9uIN.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DY1m4C2s.js → angular-ts-DF2P25Zp.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-W6Ty7v05.js → apl-Ff96wxko.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DlLVHwVk.js → astro-LbBnZna-.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-Bco3tDh4.js → blade-DltuxeBW.js} +1 -1
- package/dist/assets/web/dist/assets/{c-1LaFY_cj.js → c-DBg7yuPx.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-B-OvrOL5.js → chapel-C83hUAds.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-Ch3EPlHu.js → cobol-DB0yV_2D.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-hZuITnto.js → coffee-Bpih6Kzl.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-B8iRVo8m.js → cpp-CU00ZeRn.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DKveI7lr.js → crystal-f8ImJkux.js} +1 -1
- package/dist/assets/web/dist/assets/{css-_2_CD-rr.js → css-4VrC4ISo.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-pOdUzj6D.js → dist-TAGawIfM.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-CYMux25J.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BenMDPct.js → edge-BwFDzfS_.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-CBbdEqG6.js → elixir-wwIYIpJm.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DMub_2lY.js → elm-DyDuM3v3.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CI9He_Up.js → erb-DFJBEVrh.js} +1 -1
- package/dist/assets/web/dist/assets/{favicon-BQsePYv5.js → favicon-CSt-qDvc.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-BBfhnyF8.js → git-rebase-CuDv-44X.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-CR9Fxrau.js → glimmer-js-ZFnrm4J3.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CwFwX_XS.js → glimmer-ts-XY4Yqshp.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Daiodp1r.js → glsl-xnR8efx0.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-DZvrsEkB.js → graphql-D-gO5tnP.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BaXOzdg7.js → hack-CSxbW4ax.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CMskq_VA.js → haml-D4ar42QT.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-Dhw_f7jD.js → handlebars-CMLbDE7o.js} +1 -1
- package/dist/assets/web/dist/assets/{html-C9k99z0z.js → html-CVekU4Fs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-VckPItXN.js → html-derivative-St3HHw_N.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXU_3h9v.js → http-Mon0DWHS.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-KU24JNdp.js → hurl-Cbl5IvBs.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-FLJu5mR9.js +1 -0
- package/dist/assets/web/dist/assets/{java-BbZjTNgf.js → java-Bprck1l-.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-DXCdqcZZ.js → javascript-C73G1-UR.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-pDlsjTVp.js → jinja-C4DuRxYJ.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DsXDk66e.js → jison-B2U03GEC.js} +1 -1
- package/dist/assets/web/dist/assets/{json-tCxXfRgG.js → json-CvTZUDzx.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-B0ZYkCmi.js → jsx-DMWKCzwo.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-BLzzeI6x.js → julia-Czn7bV_3.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DvD2_60c.js → just-BBxpNfvC.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-511zn3h7.js → latex-Bw9yg5mr.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BD0BkMox.js → liquid-BRCjaydE.js} +1 -1
- package/dist/assets/web/dist/assets/{indexFormat-B-pd4r7Y.js → localIso-CNA-bhS-.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-NFicKm6N.js → lua-DUu2KIC0.js} +1 -1
- package/dist/assets/web/dist/assets/main-csihqEj5.css +1 -0
- package/dist/assets/web/dist/assets/main-mUd_TJjH.js +28 -0
- package/dist/assets/web/dist/assets/{marko-1-G_nEXK.js → marko-CZJZIlSQ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DUe3AQLp.js → mdc-BmClq6fd.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Dg379_bA.js → nginx-Bmt3xSEm.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BnFq97ZO.js → nim-Da-i0utK.js} +1 -1
- package/dist/assets/web/dist/assets/{org-F0uiwGvq.js → org-BvAre2qP.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-B0euczkl.js → perl-v9WwFFtB.js} +1 -1
- package/dist/assets/web/dist/assets/{php-DkL3k_n7.js → php-DTU4Zyze.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-L_OjjZP4.js → pug-DhTwEqTO.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BMjB00Zz.js → qml-BUD9AC_c.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DoeLdnqR.js → r-CAnu56n7.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BzYNkAWy.js → razor-Dn0P4gAq.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CPmElMk3.js → regexp-CZyBRaVW.js} +1 -1
- package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +1 -0
- package/dist/assets/web/dist/assets/{rst-DrsjgfI7.js → rst-Ckyd5UsY.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cB42ppvx.js → ruby-BBQ62fsx.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-R5Y3NM_I.js → sas-CW35hMxG.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-C-zWxV9s.js → scss-CpV8pF4J.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-oJF96aAU.js → shellscript-CRJvsmE1.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CznECKyi.js → shellsession-CyriUZxf.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-wjHLSRag.js → soy-D5QzYUeg.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-C_BM8IOW.js → sql-CUBf_Mse.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-D31HO8aQ.js → stata-B-4n4ITz.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CKCLyplA.js → surrealql-RuWZBLtr.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-3geWk2iu.js → svelte-CO5YzCsK.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DiJr_hTD.js → templ-DbkWVaBF.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BEJGWyeF.js → tex-IuxoEcId.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-C8Mzdpxx.js → ts-tags-g6dGQPjd.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-UQiSm_p3.js → tsx-WnXwiyH2.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-_Pvzkeo9.js → twig-Lx1G7PnY.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BWMkINKK.js → typescript-BQd7biLF.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Bdg9m-e7.js → typst-D2lXPHK7.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CDZrrAZ8.js → vue-4wVQD9y9.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-tAlpNhfN.js → vue-html-DZPaRte4.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-C27UF_rh.js → vue-vine-DbmZCmu-.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-CxeDr9Zh.js → xml-DV16szyo.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bpi0uUnM.js → xsl-eo5-1sk0.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BtUgZ1GN.js → yaml-BXdX8HZf.js} +1 -1
- package/dist/cli.js +3329 -1804
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-lYVcj-k7.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DSpsye5e.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CT8fI1M3.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-Cqh8pTXC.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-sjXr8iCV.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-Tz__zLzi.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CbKCL58v.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-Bnj883ii.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-D3lG-yQ9.js +0 -28
- package/dist/assets/web/dist/assets/main-Dm11o0hc.css +0 -1
- package/dist/assets/web/dist/assets/residentDiskBudget-BMBKlYRH.js +0 -1
|
@@ -151,9 +151,19 @@ export { parseDfKiB };
|
|
|
151
151
|
// ---------------------------------------------------------------------------
|
|
152
152
|
|
|
153
153
|
/** The refresh cycle snapshots mirror + checkout to R2 through the Sandbox
|
|
154
|
-
* SDK (`createBackup`)
|
|
155
|
-
*
|
|
156
|
-
*
|
|
154
|
+
* SDK (`createBackup`), which stages each archive on the container's own disk
|
|
155
|
+
* (`/var/backups/<id>.sqsh`, lz4 squashfs) before the upload and removes it
|
|
156
|
+
* after, the pair concurrently — so the budget holds room for one compressed
|
|
157
|
+
* copy of what the cycle archives, at the ratio `instanceSizing.test.ts` sizes
|
|
158
|
+
* the instance with. Since item 61 PR B the checkout archive EXCLUDES
|
|
159
|
+
* node_modules (`CHECKOUT_SNAPSHOT_EXCLUDES`): the deps are not in the
|
|
160
|
+
* cycle's staging. The deps-store entry has its own archive, taken once per
|
|
161
|
+
* lockfile key right after the entry is committed, and that one IS staged
|
|
162
|
+
* from the deps — the reserve counts it only while such a backup is in
|
|
163
|
+
* flight (`depsBackupInFlight`), never as a standing charge. Before this,
|
|
164
|
+
* the standing term multiplied the deps too: on a resident whose deps are
|
|
165
|
+
* 6.9 GiB of an 8.3 GiB archived set, five gigabytes were held for an archive
|
|
166
|
+
* of about one, and every attach was refused with 6.7 GiB free. */
|
|
157
167
|
export const SNAPSHOT_STAGING_RATIO = 0.6;
|
|
158
168
|
|
|
159
169
|
/** The fixed floor under the staging term: at least 1 GiB, or 5 % of the disk
|
|
@@ -173,13 +183,26 @@ export interface DiskReserve {
|
|
|
173
183
|
totalKiB: number;
|
|
174
184
|
}
|
|
175
185
|
|
|
186
|
+
/** What the reserve knows beyond the sample: whether a deps-store entry
|
|
187
|
+
* backup is being taken right now (item 61), whose archive is staged from
|
|
188
|
+
* the deps and so needs their share of the ratio while it runs. */
|
|
189
|
+
export interface DiskReserveInput {
|
|
190
|
+
depsBackupInFlight?: boolean;
|
|
191
|
+
}
|
|
192
|
+
|
|
176
193
|
/** `staging + floor`. Staging is computed from the MEASURED mirror and
|
|
177
|
-
* checkout (
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
* checkout (the checkout's own bytes: history + tree + build output, never
|
|
195
|
+
* the deps, which the checkout archive excludes), plus the deps while a
|
|
196
|
+
* deps-store entry backup is in flight; an unmeasured part counts as 0 there
|
|
197
|
+
* — the floor still stands, and `projectThreadCostKiB` refuses to project
|
|
198
|
+
* from missing parts, so an unmeasured resident never admits on a guess. */
|
|
199
|
+
export function diskReserveKiB(
|
|
200
|
+
sample: Pick<DiskSample, "totalKiB" | "parts">,
|
|
201
|
+
input: DiskReserveInput = {},
|
|
202
|
+
): DiskReserve {
|
|
203
|
+
const staged = (sample.parts.mirror ?? 0) + (sample.parts.checkout ?? 0);
|
|
204
|
+
const depsStaged = input.depsBackupInFlight ? (sample.parts.deps ?? 0) : 0;
|
|
205
|
+
const stagingKiB = Math.round((staged + depsStaged) * SNAPSHOT_STAGING_RATIO);
|
|
183
206
|
const floorKiB = Math.max(DISK_FLOOR_MIN_KIB, Math.round(sample.totalKiB * DISK_FLOOR_FRACTION));
|
|
184
207
|
return { stagingKiB, floorKiB, totalKiB: stagingKiB + floorKiB };
|
|
185
208
|
}
|
|
@@ -261,11 +284,14 @@ export function checkDiskAdmission(input: {
|
|
|
261
284
|
committedKiB?: number;
|
|
262
285
|
diskBudgetMb?: number;
|
|
263
286
|
kind: ThreadCostKind;
|
|
287
|
+
/** A deps-store entry backup is in flight: its archive is staged from the
|
|
288
|
+
* deps, so the reserve holds their share for as long as it runs. */
|
|
289
|
+
depsBackupInFlight?: boolean;
|
|
264
290
|
}): AdmissionVerdict {
|
|
265
291
|
const rawFree = Math.max(0, (input.freeKiB ?? input.sample.freeKiB) - (input.committedKiB ?? 0));
|
|
266
292
|
const live = { ...input.sample, freeKiB: rawFree, usedKiB: input.sample.totalKiB - rawFree };
|
|
267
293
|
const { capacityKiB, freeKiB, capped } = effectiveFreeKiB(live, input.diskBudgetMb);
|
|
268
|
-
const reserve = diskReserveKiB(input.sample);
|
|
294
|
+
const reserve = diskReserveKiB(input.sample, { depsBackupInFlight: input.depsBackupInFlight });
|
|
269
295
|
const projectedKiB = projectThreadCostKiB(input.sample.parts, input.kind);
|
|
270
296
|
const headroomKiB = freeKiB - reserve.totalKiB - (projectedKiB ?? 0);
|
|
271
297
|
const math: AdmissionMath = { projectedKiB, kind: input.kind, freeKiB, capacityKiB, capped, reserve, headroomKiB };
|
|
@@ -401,6 +427,9 @@ export function diskPressureReason(input: {
|
|
|
401
427
|
verdict: Extract<AdmissionVerdict, { fits: false }>;
|
|
402
428
|
evicted: ReadonlyArray<{ freedKiB: number | null }>;
|
|
403
429
|
kept: ReadonlyArray<{ why: DiskKeepWhy }>;
|
|
430
|
+
/** Deps-store spares removed first under the pressure (item 55), with the
|
|
431
|
+
* bytes each gave back. */
|
|
432
|
+
spares?: ReadonlyArray<{ freedKiB: number }>;
|
|
404
433
|
}): string {
|
|
405
434
|
const { math } = input.verdict;
|
|
406
435
|
const need =
|
|
@@ -412,10 +441,15 @@ export function diskPressureReason(input: {
|
|
|
412
441
|
`${DISK_PRESSURE_REASON}: need ${need}, but ${formatGiB(math.freeKiB)} free${cap} minus the ${formatGiB(math.reserve.totalKiB)} reserve ` +
|
|
413
442
|
`(snapshot staging ${formatGiB(math.reserve.stagingKiB)} + floor ${formatGiB(math.reserve.floorKiB)}) leaves ${formatGiB(Math.max(0, math.freeKiB - math.reserve.totalKiB))} — short by ${formatGiB(input.verdict.shortfallKiB)}`,
|
|
414
443
|
];
|
|
444
|
+
const spares = input.spares ?? [];
|
|
445
|
+
if (spares.length > 0) {
|
|
446
|
+
const freed = spares.reduce((a, e) => a + e.freedKiB, 0);
|
|
447
|
+
parts.push(`evicted ${spares.length} deps-store spare(s) (${formatGiB(freed)} back)`);
|
|
448
|
+
}
|
|
415
449
|
if (input.evicted.length > 0) {
|
|
416
450
|
const freed = input.evicted.reduce((a, e) => a + (e.freedKiB ?? 0), 0);
|
|
417
451
|
parts.push(`evicted ${input.evicted.length} idle tree(s) (${formatGiB(freed)} back)`);
|
|
418
|
-
} else parts.push("evicted nothing");
|
|
452
|
+
} else parts.push(spares.length > 0 ? "evicted no idle tree" : "evicted nothing");
|
|
419
453
|
if (input.kept.length > 0) {
|
|
420
454
|
const counts = new Map<DiskKeepWhy, number>();
|
|
421
455
|
for (const k of input.kept) counts.set(k.why, (counts.get(k.why) ?? 0) + 1);
|
|
@@ -42,19 +42,45 @@ export function wantShaForBinding(input: {
|
|
|
42
42
|
return input.wantSha;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
45
|
+
/** Why the attach fetches the mirror before cloning the thread tree. The
|
|
46
|
+
* Worker treats the three differently when the fetch fails: a `missing-ref`
|
|
47
|
+
* or `stale-tip` fetch that fails fails the attach, as it always did; a
|
|
48
|
+
* `returnable-ref` fetch only verifies a ref the mirror holds, so its
|
|
49
|
+
* failure is one log line and the attach goes on with the mirror's ref. */
|
|
50
|
+
export type FetchReason = "missing-ref" | "stale-tip" | "returnable-ref";
|
|
51
|
+
|
|
52
|
+
/** Whether — and why — the attach must fetch the mirror before cloning the
|
|
53
|
+
* thread tree; null when the mirror is good enough as it stands.
|
|
54
|
+
* - the ref is not in the mirror → `missing-ref` (the oldest rule);
|
|
47
55
|
* - a `wantSha` was named and the mirror's tip of the ref is not that
|
|
48
|
-
* commit →
|
|
49
|
-
* -
|
|
56
|
+
* commit → `stale-tip`;
|
|
57
|
+
* - the bound ref is one the binding could return from — a branch a rebind
|
|
58
|
+
* moved it onto, or one the thread's own runs pushed (`canReturnToDefault`,
|
|
59
|
+
* resident-repos.md item 16) → `returnable-ref`: such a branch dies when
|
|
60
|
+
* its pull request merges, and the deletion reaches the mirror only through
|
|
61
|
+
* a `fetch --prune`. Trusting the mirror because it still holds the ref
|
|
62
|
+
* would provision a tree at the deleted branch's stale tip until the
|
|
63
|
+
* refresh cycle's prune caught up — whether the thread returned to the
|
|
64
|
+
* default would depend on where in the cycle its follow-up landed. So the
|
|
65
|
+
* ref is verified against the origin at every attach, and the return is
|
|
66
|
+
* decided at the first attach after the deletion. The default cannot be
|
|
67
|
+
* lost and a ref a person named that the thread never pushed has no way
|
|
68
|
+
* back, so neither pays this fetch: the refresh cycle is their freshness.
|
|
69
|
+
* - otherwise null.
|
|
50
70
|
* A fetch that STILL leaves the tip elsewhere (a push racing this attach, or
|
|
51
71
|
* a force-push) is not this function's concern: the attach proceeds on the
|
|
52
72
|
* fetched tip and reports it, and the reviewed-head guard decides what a
|
|
53
73
|
* review of it may do. */
|
|
54
|
-
export function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
export function mirrorFetchReason(input: {
|
|
75
|
+
refExists: boolean;
|
|
76
|
+
mirrorSha?: string;
|
|
77
|
+
wantSha: string | null;
|
|
78
|
+
returnable: boolean;
|
|
79
|
+
}): FetchReason | null {
|
|
80
|
+
if (!input.refExists) return "missing-ref";
|
|
81
|
+
if (input.wantSha !== null && input.mirrorSha !== input.wantSha) return "stale-tip";
|
|
82
|
+
if (input.returnable) return "returnable-ref";
|
|
83
|
+
return null;
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
/** What the attach checks out, once the mirror is as fresh as it will get. */
|
|
@@ -35,15 +35,21 @@
|
|
|
35
35
|
* Every refusal is named in the attach answer so the bot can say why the
|
|
36
36
|
* follow-up runs where it does.
|
|
37
37
|
*
|
|
38
|
-
* The second movement: a
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* binding
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
38
|
+
* The second movement: a binding can sit on a branch that dies — the pull
|
|
39
|
+
* request merges and the branch is deleted. A binding left on it would fail
|
|
40
|
+
* every later attach (`unknown-ref`) for the thread's whole life. So a
|
|
41
|
+
* binding whose ref the mirror no longer holds after a fetch goes back to the
|
|
42
|
+
* default (`canReturnToDefault`, `returnToDefault`) when the gone ref is
|
|
43
|
+
* EITHER the branch a rebind moved it onto OR one of the thread's own
|
|
44
|
+
* branches — a branch this thread's runs pushed (`ownBranches`), whatever
|
|
45
|
+
* `boundBy` says: a ship unit's coding child binds its unit branch by name,
|
|
46
|
+
* pushes it and opens the pull request, and the unit's merge deletes it. The
|
|
47
|
+
* attach provisions the tree at the default, clean, and the thread is
|
|
48
|
+
* default-bound again (`boundBy: default` — the default was chosen for want
|
|
49
|
+
* of the branch it was on), so a later own pull request may move it once
|
|
50
|
+
* more. A ref a person named that the thread never pushed keeps the
|
|
51
|
+
* `unknown-ref` refusal: a person's branch that is gone is not this thread's
|
|
52
|
+
* finished work, and the resident never swaps it for the default unasked. */
|
|
47
53
|
|
|
48
54
|
/** The pull request the thread's own run opened, and its head branch — the
|
|
49
55
|
* reason a caller's refHint is that branch. */
|
|
@@ -136,9 +142,15 @@ export function rememberOwnBranches(
|
|
|
136
142
|
return [...kept, ...added].slice(-OWN_BRANCHES_MAX);
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/** The binding's memory of `ref` as a branch the thread's own runs pushed —
|
|
146
|
+
* the pull request it heads and when it was told — or none. */
|
|
147
|
+
export function ownBranchOf(binding: { ownBranches?: readonly OwnBranch[] }, ref: string): OwnBranch | undefined {
|
|
148
|
+
return (binding.ownBranches ?? []).find((b) => b.ref === ref);
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
/** Whether the thread's own runs pushed `ref`, as the binding remembers it. */
|
|
140
152
|
export function isOwnBranch(binding: { ownBranches?: readonly OwnBranch[] }, ref: string): boolean {
|
|
141
|
-
return (binding
|
|
153
|
+
return ownBranchOf(binding, ref) !== undefined;
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
/** How a binding's ref was chosen: the repo default for want of a named
|
|
@@ -171,8 +183,12 @@ export interface Rebound {
|
|
|
171
183
|
returnedAt?: string;
|
|
172
184
|
}
|
|
173
185
|
|
|
174
|
-
/** The move back
|
|
175
|
-
*
|
|
186
|
+
/** The move back (the second movement), on the binding and in the attach
|
|
187
|
+
* answer: from the branch that is gone, to the default, for the pull request
|
|
188
|
+
* whose branch it was — the rebind's, or the one the binding remembers the
|
|
189
|
+
* branch heading — when. On the binding it is the LAST move back: a rebind's
|
|
190
|
+
* own record keeps its `returnedAt` beside it, and a binding never rebound
|
|
191
|
+
* (its unit branch bound by name and pushed) has only this. */
|
|
176
192
|
export interface Returned {
|
|
177
193
|
from: string;
|
|
178
194
|
to: string;
|
|
@@ -199,6 +215,8 @@ export interface RebindableBinding {
|
|
|
199
215
|
evicted?: boolean;
|
|
200
216
|
boundBy?: BoundBy;
|
|
201
217
|
rebound?: Rebound;
|
|
218
|
+
/** The binding's last move back to the default (the second movement). */
|
|
219
|
+
returned?: Returned;
|
|
202
220
|
/** The branches the thread's own runs pushed, handed over at each release. */
|
|
203
221
|
ownBranches?: OwnBranch[];
|
|
204
222
|
}
|
|
@@ -320,29 +338,68 @@ export function rebindVerdict(plan: { to: string; pr: number; own?: boolean }, t
|
|
|
320
338
|
return { kind: "rebind" };
|
|
321
339
|
}
|
|
322
340
|
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
|
|
330
|
-
|
|
341
|
+
/** What the second movement reads off a binding. */
|
|
342
|
+
export type ReturnableBinding = Pick<RebindableBinding, "ref" | "boundBy" | "rebound" | "ownBranches">;
|
|
343
|
+
|
|
344
|
+
/** The way back a binding whose ref is gone may take, if any — the one place
|
|
345
|
+
* the second movement is decided, so the predicate and the move cannot
|
|
346
|
+
* disagree: `rebind` — a rebind moved the binding onto the branch (bound by
|
|
347
|
+
* default in the first place, still on that branch, the move not yet
|
|
348
|
+
* returned); `own` — the branch is one this thread's own runs pushed, as the
|
|
349
|
+
* binding remembers it, whatever `boundBy` says. A binding on the default
|
|
350
|
+
* cannot lose its ref, and a ref a person named that the thread never pushed
|
|
351
|
+
* has no way back: that branch is the person's to sort out. */
|
|
352
|
+
function wayBack(
|
|
353
|
+
binding: ReturnableBinding,
|
|
331
354
|
defaultRef: string,
|
|
332
|
-
):
|
|
355
|
+
): { kind: "rebind"; rebound: Rebound } | { kind: "own"; branch: OwnBranch } | undefined {
|
|
356
|
+
if (binding.ref === defaultRef) return undefined;
|
|
333
357
|
const r = binding.rebound;
|
|
334
|
-
if (
|
|
335
|
-
|
|
358
|
+
if (
|
|
359
|
+
r !== undefined &&
|
|
360
|
+
r.returnedAt === undefined &&
|
|
361
|
+
binding.ref === r.to &&
|
|
362
|
+
boundByOf(binding, defaultRef) === "default"
|
|
363
|
+
) {
|
|
364
|
+
return { kind: "rebind", rebound: r };
|
|
365
|
+
}
|
|
366
|
+
const branch = ownBranchOf(binding, binding.ref);
|
|
367
|
+
return branch === undefined ? undefined : { kind: "own", branch };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Whether a binding whose ref the mirror no longer holds goes back to the
|
|
371
|
+
* default branch (the second movement): a binding a rebind moved onto its
|
|
372
|
+
* own pull request's branch, or one on a branch this thread itself pushed.
|
|
373
|
+
* Anything else keeps the attach's `unknown-ref` refusal. */
|
|
374
|
+
export function canReturnToDefault(binding: ReturnableBinding, defaultRef: string): boolean {
|
|
375
|
+
return wayBack(binding, defaultRef) !== undefined;
|
|
336
376
|
}
|
|
337
377
|
|
|
338
|
-
/** The move back: the binding's ref becomes the default
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
|
|
342
|
-
|
|
378
|
+
/** The move back: the binding's ref becomes the default, the binding reads as
|
|
379
|
+
* bound by default (the default was chosen for want of the branch it was on,
|
|
380
|
+
* so its next own pull request may move it — a binding that named its unit
|
|
381
|
+
* branch included), a rebind's move is stamped returned, and the last move
|
|
382
|
+
* back is recorded. The pull request named is the rebind's, else the one the
|
|
383
|
+
* binding remembers the gone branch heading. Undefined for a binding
|
|
384
|
+
* `canReturnToDefault` does not admit: nothing to write. */
|
|
385
|
+
export function returnToDefault<B extends ReturnableBinding>(
|
|
386
|
+
binding: B,
|
|
343
387
|
defaultRef: string,
|
|
344
388
|
at: string,
|
|
345
|
-
): { binding: B; returned: Returned } {
|
|
346
|
-
const
|
|
347
|
-
|
|
389
|
+
): { binding: B; returned: Returned } | undefined {
|
|
390
|
+
const way = wayBack(binding, defaultRef);
|
|
391
|
+
if (way === undefined) return undefined;
|
|
392
|
+
const pr = way.kind === "rebind" ? way.rebound.pr : way.branch.pr;
|
|
393
|
+
const returned: Returned = { from: binding.ref, to: defaultRef, pr, at };
|
|
394
|
+
const rebound = way.kind === "rebind" ? { ...way.rebound, returnedAt: at } : binding.rebound;
|
|
395
|
+
return {
|
|
396
|
+
binding: {
|
|
397
|
+
...binding,
|
|
398
|
+
ref: defaultRef,
|
|
399
|
+
boundBy: "default",
|
|
400
|
+
...(rebound !== undefined ? { rebound } : {}),
|
|
401
|
+
returned,
|
|
402
|
+
},
|
|
403
|
+
returned,
|
|
404
|
+
};
|
|
348
405
|
}
|
|
@@ -125,6 +125,78 @@ export function fleetBusyExhaustedMessage(waitedMs: number): string {
|
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// A container that is still starting (docs/reference/specs/execution.md item 23).
|
|
130
|
+
//
|
|
131
|
+
// Why this exists: a thread's first request finds no running container, and
|
|
132
|
+
// the SDK's first exec then carries the whole start — the platform's instance
|
|
133
|
+
// grant (its default wait 30 s), the image pull on a machine that has not seen
|
|
134
|
+
// this image, the microVM boot and the runtime's port (90 s) — before the
|
|
135
|
+
// command runs. The executor's per-send deadline for a 60 s command is 90 s,
|
|
136
|
+
// so every fresh-sandbox run died with "gave no answer within 90s" while its
|
|
137
|
+
// container came up a minute later and sat idle. The Worker now names the
|
|
138
|
+
// condition instead: it starts the container in the background and answers
|
|
139
|
+
// `sandbox-starting` at once, the executor waits on the token exactly as it
|
|
140
|
+
// waits on `fleet-busy` — the identical request re-sent, nothing ran — under a
|
|
141
|
+
// start budget of its own, and the command runs once the container is up.
|
|
142
|
+
|
|
143
|
+
/** The machine token the executor waits on, beside `fleet-busy`. */
|
|
144
|
+
export const SANDBOX_STARTING_REASON = "sandbox-starting" as const;
|
|
145
|
+
|
|
146
|
+
/** What the token means, in the words the model and the operator see. */
|
|
147
|
+
export const SANDBOX_STARTING_EXPLANATION =
|
|
148
|
+
"the thread's sandbox container is starting (image pull, boot, runtime) — nothing ran yet; the request is re-sent once it is up";
|
|
149
|
+
|
|
150
|
+
/** The executor waits at most this long for a container to start, whatever
|
|
151
|
+
* the command's own budget: the platform's own start allowances (30 s for an
|
|
152
|
+
* instance, 90 s for the port) plus a slow image pull fit inside it, and a
|
|
153
|
+
* 60 s command is never killed by a two-minute start it did not cause. */
|
|
154
|
+
export const SANDBOX_START_WAIT_MAX_MS = 5 * 60_000;
|
|
155
|
+
|
|
156
|
+
/** Backoff between re-sends while a container starts: 5 s, 10 s, then 15 s.
|
|
157
|
+
* A start takes tens of seconds, not minutes, so the poll is denser than the
|
|
158
|
+
* fleet wait's and a ready container is used within 15 s of coming up. */
|
|
159
|
+
export const SANDBOX_START_BACKOFF_MS: readonly number[] = [5_000, 10_000, 15_000];
|
|
160
|
+
|
|
161
|
+
/** The reasons whose answers the executor re-sends after a wait. Every other
|
|
162
|
+
* `reason` — or none — is an ordinary failure after one send. */
|
|
163
|
+
export type WaitReason = typeof FLEET_BUSY_REASON | typeof SANDBOX_STARTING_REASON;
|
|
164
|
+
|
|
165
|
+
export function isWaitReason(reason: unknown): reason is WaitReason {
|
|
166
|
+
return reason === FLEET_BUSY_REASON || reason === SANDBOX_STARTING_REASON;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The Worker's answer on /read and /write (sent as HTTP 503) while the
|
|
170
|
+
* container starts: the token, and the start's own phase as the cause. */
|
|
171
|
+
export function sandboxStartingAnswer(cause: string): { error: string; reason: typeof SANDBOX_STARTING_REASON } {
|
|
172
|
+
return {
|
|
173
|
+
error: `${SANDBOX_STARTING_REASON}: ${SANDBOX_STARTING_EXPLANATION} (${cause})`,
|
|
174
|
+
reason: SANDBOX_STARTING_REASON,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The Worker's answer on /exec, in-body under the streamed HTTP 200 in the
|
|
179
|
+
* item-3 dual shape, like `fleetBusyExecAnswer`. */
|
|
180
|
+
export function sandboxStartingExecAnswer(cause: string): {
|
|
181
|
+
error: string;
|
|
182
|
+
reason: typeof SANDBOX_STARTING_REASON;
|
|
183
|
+
stdout: "";
|
|
184
|
+
stderr: string;
|
|
185
|
+
exitCode: 127;
|
|
186
|
+
} {
|
|
187
|
+
const { error, reason } = sandboxStartingAnswer(cause);
|
|
188
|
+
return { error, reason, stdout: "", stderr: error, exitCode: 127 };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The message `ExecCapacityError` carries when a container did not start
|
|
192
|
+
* inside the start budget: the wait, and what to do. */
|
|
193
|
+
export function startWaitExhaustedMessage(waitedMs: number): string {
|
|
194
|
+
return (
|
|
195
|
+
`sandbox not ready — the thread's container did not finish starting within ${Math.round(waitedMs / 1000)}s; ` +
|
|
196
|
+
"try again in a few minutes"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
128
200
|
/** The text the Worker carries in-body for a thrown value: the SDK's own
|
|
129
201
|
* message when it has one, else a sentence that says the SDK gave none —
|
|
130
202
|
* naming the error's name and code, and the one condition known to produce
|
|
@@ -76,3 +76,36 @@ export function recycledMidCommandMessage(elapsedMs: number, msg: string, certai
|
|
|
76
76
|
`check /workspace before continuing: it is empty if the container was replaced (re-clone), intact if only its runtime restarted (${msg})`
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/** The redirects a detached job's wrapper must carry so the command that
|
|
81
|
+
* forked it owns none of its descriptors — the same string the harness seam
|
|
82
|
+
* uses for its own start (`DETACHED_STDIO` in src/core/harness/container.ts;
|
|
83
|
+
* a test holds the two equal). Without them the runtime waits for an end of
|
|
84
|
+
* output the detached child never gives (item 24). */
|
|
85
|
+
export const DETACH_REDIRECTS = "</dev/null >/dev/null 2>&1";
|
|
86
|
+
|
|
87
|
+
/** The way a command detaches a job that must outlive it, as the exit-124
|
|
88
|
+
* hint and the docs spell it: the job's own output into a file, the
|
|
89
|
+
* wrapper's stdio to /dev/null. */
|
|
90
|
+
export const DETACH_HINT = `setsid -f sh -c '<command> > /tmp/job.log 2>&1' ${DETACH_REDIRECTS}`;
|
|
91
|
+
|
|
92
|
+
/** How long past the process's exit the Durable Object waits for its output
|
|
93
|
+
* stream to end before it answers with the exit code alone. The runtime
|
|
94
|
+
* reports the stream's end only when every holder of the command's stdout
|
|
95
|
+
* and stderr is gone; a detached child that inherited them holds it open
|
|
96
|
+
* for its own lifetime. Twenty seconds covers coreutils `timeout -k 10`'s
|
|
97
|
+
* SIGKILL follow-up and the runtime's teardown, and stays inside the
|
|
98
|
+
* executor's per-send margin (EXEC_CALL_MARGIN_MS, 30 s) so this answer
|
|
99
|
+
* reaches the bot before it gives the command up. */
|
|
100
|
+
export const OUTPUT_AFTER_EXIT_MS = 20_000;
|
|
101
|
+
|
|
102
|
+
/** The stderr line an exec answers with when its process exited but its
|
|
103
|
+
* output never ended: what happened, why the output is missing, and how to
|
|
104
|
+
* detach a job so it does not happen again. */
|
|
105
|
+
export function heldOutputNote(exitCode: number): string {
|
|
106
|
+
return (
|
|
107
|
+
`the command exited (code ${exitCode}) but its output could not be collected: a process it started still holds its stdout or stderr open ` +
|
|
108
|
+
`(a background job started without redirecting the wrapper's own stdio); its output is lost to this call, the job itself is still running — ` +
|
|
109
|
+
`detach with \`${DETACH_HINT}\` so the command's descriptors close when it does`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The start gate of a thread's sandbox (docs/reference/specs/execution.md
|
|
2
|
+
// item 23): the Durable Object's decision, on every request, between running
|
|
3
|
+
// the operation and answering `sandbox-starting`. Deliberately free of node:
|
|
4
|
+
// imports so wrangler can bundle it into the sandbox Worker, like
|
|
5
|
+
// sandboxErrors.ts and sandboxIdle.ts.
|
|
6
|
+
//
|
|
7
|
+
// Why: a thread's first request finds no running container, and the SDK's
|
|
8
|
+
// first exec then carries the whole start — the platform's instance grant,
|
|
9
|
+
// the image pull, the microVM boot, the runtime's port — before the command
|
|
10
|
+
// runs; the executor's per-send deadline for a 60 s command is 90 s, so every
|
|
11
|
+
// fresh-sandbox run died with "gave no answer within 90s" while its container
|
|
12
|
+
// came up a minute later. The gate starts the container in the background
|
|
13
|
+
// through a warm-up the host provides, answers the named token at once, and
|
|
14
|
+
// lets the operation through only once the container is up. A warm-up that
|
|
15
|
+
// fails hands its error to the next request, so a full fleet or a silent
|
|
16
|
+
// runtime keeps its own name (item 14, item 9) — the gate never swallows it.
|
|
17
|
+
|
|
18
|
+
/** What the gate needs from the Durable Object. */
|
|
19
|
+
export interface StartGateHost {
|
|
20
|
+
/** `ctx.container?.running`: true once the platform runs the container,
|
|
21
|
+
* false when it is stopped, undefined when there is no container binding
|
|
22
|
+
* (treated as not running: the warm-up will say what is wrong). */
|
|
23
|
+
containerRunning(): boolean | undefined;
|
|
24
|
+
/** Start the container and wait for its runtime: one trivial command
|
|
25
|
+
* through the SDK, which does the start itself. Resolves when the runtime
|
|
26
|
+
* answered; rejects with the SDK's own error otherwise. */
|
|
27
|
+
warmUp(): Promise<void>;
|
|
28
|
+
now(): number;
|
|
29
|
+
log(event: Record<string, unknown>): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The gate's answer when the operation cannot run yet: the phase the start
|
|
33
|
+
* is in, in words, for the answer's cause. */
|
|
34
|
+
export type StartingCause = "container not running; starting it" | "container starting";
|
|
35
|
+
|
|
36
|
+
export class StartGate {
|
|
37
|
+
private starting: Promise<void> | null = null;
|
|
38
|
+
private startedAt = 0;
|
|
39
|
+
private failure: { error: unknown } | null = null;
|
|
40
|
+
|
|
41
|
+
constructor(private readonly host: StartGateHost) {}
|
|
42
|
+
|
|
43
|
+
/** Run `op` when the container is up; otherwise answer `starting(cause)`
|
|
44
|
+
* at once — beginning the warm-up on the first such request — and let the
|
|
45
|
+
* executor's wait re-send. A warm-up that failed is thrown here once, on
|
|
46
|
+
* the next request, so the caller classifies it as it would any SDK error
|
|
47
|
+
* (a full fleet, a silent control port); the request after that starts a
|
|
48
|
+
* fresh warm-up if the container is still not running. */
|
|
49
|
+
async through<T, S>(op: () => Promise<T>, starting: (cause: StartingCause) => S): Promise<T | S> {
|
|
50
|
+
if (this.failure) {
|
|
51
|
+
const { error } = this.failure;
|
|
52
|
+
this.failure = null;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
if (this.starting) return starting("container starting");
|
|
56
|
+
if (this.host.containerRunning() !== true) {
|
|
57
|
+
this.begin();
|
|
58
|
+
return starting("container not running; starting it");
|
|
59
|
+
}
|
|
60
|
+
return op();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether a warm-up is in flight — for the wiring test and the card. */
|
|
64
|
+
get isStarting(): boolean {
|
|
65
|
+
return this.starting !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private begin(): void {
|
|
69
|
+
this.startedAt = this.host.now();
|
|
70
|
+
this.host.log({ event: "sandbox.starting" });
|
|
71
|
+
this.starting = this.host
|
|
72
|
+
.warmUp()
|
|
73
|
+
.then(
|
|
74
|
+
() => {
|
|
75
|
+
this.host.log({ event: "sandbox.started", durationMs: this.host.now() - this.startedAt });
|
|
76
|
+
},
|
|
77
|
+
(error: unknown) => {
|
|
78
|
+
this.host.log({
|
|
79
|
+
event: "sandbox.start-failed",
|
|
80
|
+
durationMs: this.host.now() - this.startedAt,
|
|
81
|
+
error: String(error),
|
|
82
|
+
});
|
|
83
|
+
this.failure = { error };
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
.finally(() => {
|
|
87
|
+
this.starting = null;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|