@coreplane/switchboard 1.238.0 → 1.240.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/config/config.example.yaml +6 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +115 -1
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +7 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +223 -65
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +8 -1
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +337 -44
- package/dist/assets/deploy/cloudflare-sandbox/wrangler.template.jsonc +19 -0
- package/dist/assets/deploy/secrets.manifest.json +4 -4
- package/dist/assets/package-lock.json +207 -3
- package/dist/assets/package.json +2 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +82 -0
- package/dist/assets/src/core/authz/actor.ts +33 -2
- package/dist/assets/src/core/authz/policy.ts +6 -0
- package/dist/assets/src/core/coordinator/contract.ts +12 -1
- package/dist/assets/src/core/coordinator/driver.ts +13 -0
- package/dist/assets/src/core/costs.ts +1114 -0
- package/dist/assets/src/core/costsSnapshotStore.ts +209 -0
- package/dist/assets/src/core/delivery.ts +4 -11
- package/dist/assets/src/core/deliverySnapshotStore.ts +12 -28
- package/dist/assets/src/core/ingressTokens.ts +27 -13
- package/dist/assets/src/core/runLedger/types.ts +3 -0
- package/dist/assets/src/core/runRecord.ts +10 -3
- package/dist/assets/src/core/schedules.ts +2 -1
- package/dist/assets/src/core/ship/coordinator.ts +70 -0
- package/dist/assets/src/core/snapshotAge.ts +30 -0
- package/dist/assets/src/core/stateWorkerRef.ts +41 -0
- package/dist/assets/src/execution/residentAutoRebuild.ts +73 -0
- package/dist/assets/src/execution/residentRefresh.ts +25 -3
- package/dist/assets/src/execution/restoreWaiters.ts +61 -0
- package/dist/assets/src/execution/seedPlan.ts +267 -0
- package/dist/assets/src/mcp/registry.ts +7 -0
- package/dist/assets/web/dist/.vite/manifest.json +507 -427
- package/dist/assets/web/dist/assets/AppShell-CM_fh-WW.js +1 -0
- package/dist/assets/web/dist/assets/CostsPage-BD3w5j_S.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-BtvzKA1m.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-BU6aRCSe.js +2 -0
- package/dist/assets/web/dist/assets/NotFoundPage-DwOX1HaZ.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CrfCd8fx.js → ResidentDetailPage-CUAOh5d9.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-B3KUlYy2.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-Dulcbg25.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-DGoGrQZq.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-37W7OZbC.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-XpCqeWu4.js → RunsTabs-BnkvCBrM.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-C1y9jgAk.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-ggfB-Rqr.js +1 -0
- package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +1 -0
- package/dist/assets/web/dist/assets/Slideover-CX-LEklF.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-DkXALvKf.js → StatusDot-CiByEtoR.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DtLIyi5P.js → Tooltip-DyRxk8A8.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CnWyOizV.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-DYtV9uIN.js → angular-html-DAQ_1T1v.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DF2P25Zp.js → angular-ts-D4kxRVp1.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-Ff96wxko.js → apl-Dy07l64y.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-LbBnZna-.js → astro-DZIbTlc7.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-DltuxeBW.js → blade-CmSXWX3l.js} +1 -1
- package/dist/assets/web/dist/assets/{c-DBg7yuPx.js → c-ggzVdJLK.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-C83hUAds.js → chapel-Bo1wktsG.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DB0yV_2D.js → cobol-DJUmtKM2.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-Bpih6Kzl.js → coffee-BZWOVz4C.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CU00ZeRn.js → cpp-Dtk39yMw.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-f8ImJkux.js → crystal-CGf_-Zoj.js} +1 -1
- package/dist/assets/web/dist/assets/{css-4VrC4ISo.js → css-CX2fcL4u.js} +1 -1
- package/dist/assets/web/dist/assets/delivery-CTdOqpVK.js +1 -0
- package/dist/assets/web/dist/assets/{dist-DtA7JiH5.js → dist-D8TaC961.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BwFDzfS_.js → edge-BYS8FtgQ.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-wwIYIpJm.js → elixir-fhUpi3nO.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DyDuM3v3.js → elm-DAQNiwsN.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-DFJBEVrh.js → erb-CWzEys6U.js} +1 -1
- package/dist/assets/web/dist/assets/eventSource-tRprhqm7.js +1 -0
- package/dist/assets/web/dist/assets/favicon-D1hkyC5j.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-CuDv-44X.js → git-rebase-9rY3p-Rw.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-ZFnrm4J3.js → glimmer-js-tY7hy0nE.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-XY4Yqshp.js → glimmer-ts-CTrLv5JF.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-xnR8efx0.js → glsl-BRJbiDfq.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-D-gO5tnP.js → graphql-B-MtVLh2.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-CSxbW4ax.js → hack-BtIkOypQ.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-D4ar42QT.js → haml-CoOH6gfa.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-CMLbDE7o.js → handlebars-DgrRu-GP.js} +1 -1
- package/dist/assets/web/dist/assets/{html-CVekU4Fs.js → html-DZaaI7ca.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-St3HHw_N.js → html-derivative-DgHvT-qZ.js} +1 -1
- package/dist/assets/web/dist/assets/{http-Mon0DWHS.js → http-CvAd51yz.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-Cbl5IvBs.js → hurl-CXGqAfzx.js} +1 -1
- package/dist/assets/web/dist/assets/{localIso-CNA-bhS-.js → indexFormat-B-pd4r7Y.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-FLJu5mR9.js → indexRow-Cse3JGb0.js} +1 -1
- package/dist/assets/web/dist/assets/{java-Bprck1l-.js → java-CIQStIXF.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-C73G1-UR.js → javascript-WbH8s7E3.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-C4DuRxYJ.js → jinja-DzyWEVKC.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-B2U03GEC.js → jison-CrT-zCa-.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CvTZUDzx.js → json-lHjpjXMQ.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-DMWKCzwo.js → jsx-WTwYCHW6.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-Czn7bV_3.js → julia-Bqpk9H7E.js} +1 -1
- package/dist/assets/web/dist/assets/{just-BBxpNfvC.js → just-LdXk1r3m.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-Bw9yg5mr.js → latex-h_y2A5KD.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BRCjaydE.js → liquid-CY7bF2_p.js} +1 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +1 -0
- package/dist/assets/web/dist/assets/{lua-DUu2KIC0.js → lua-oQ7XVZOY.js} +1 -1
- package/dist/assets/web/dist/assets/main-CGO6fQ8k.js +28 -0
- package/dist/assets/web/dist/assets/main-O8UXRuEv.css +1 -0
- package/dist/assets/web/dist/assets/{marko-CZJZIlSQ.js → marko-zv_qZ4QV.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-BmClq6fd.js → mdc-Dl4kL_YK.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Bmt3xSEm.js → nginx-CAHCZj4F.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-Da-i0utK.js → nim-BOtmwCJH.js} +1 -1
- package/dist/assets/web/dist/assets/{org-BvAre2qP.js → org-DspQk3HQ.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-v9WwFFtB.js → perl-CAXmvVl0.js} +1 -1
- package/dist/assets/web/dist/assets/{php-DTU4Zyze.js → php-CMUx4VyM.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-DhTwEqTO.js → pug-B55xVJTv.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BUD9AC_c.js → qml-DsNKCCRk.js} +1 -1
- package/dist/assets/web/dist/assets/{r-CAnu56n7.js → r-DbhP0VvO.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-Dn0P4gAq.js → razor-BCpE6P-p.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CZyBRaVW.js → regexp-BllsjiN4.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-Ckyd5UsY.js → rst-CO5Rpq9V.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-BBQ62fsx.js → ruby-C2A4OgQq.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-CW35hMxG.js → sas-Butzoyql.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-CpV8pF4J.js → scss-PEUwJBpf.js} +1 -1
- package/dist/assets/web/dist/assets/settingsApi-priOPiwu.js +1 -0
- package/dist/assets/web/dist/assets/{shellscript-CRJvsmE1.js → shellscript-3Yhq0khE.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CyriUZxf.js → shellsession-04Hkb7m3.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-D5QzYUeg.js → soy-BnId73cP.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-CUBf_Mse.js → sql-BKwdc0ch.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-DutHNghE.js +9 -0
- package/dist/assets/web/dist/assets/{stata-B-4n4ITz.js → stata-BPPtNkay.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-RuWZBLtr.js → surrealql-CDPChcYP.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-CO5YzCsK.js → svelte-B15DIQ-z.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DbkWVaBF.js → templ-D-rp_AiB.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-IuxoEcId.js → tex-BnVDCD7p.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-g6dGQPjd.js → ts-tags-cpujdyAR.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-WnXwiyH2.js → tsx-D1vO4L72.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-Lx1G7PnY.js → twig-oQGnQGRc.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BQd7biLF.js → typescript-CKSFpXiH.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-D2lXPHK7.js → typst-DKHFo8BF.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-4wVQD9y9.js → vue-BstokpiS.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DZPaRte4.js → vue-html-DpCp0_Sz.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-DbmZCmu-.js → vue-vine-y5d5wUb1.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-DV16szyo.js → xml-Br2hx5Ax.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-eo5-1sk0.js → xsl-Bc8Cobap.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BXdX8HZf.js → yaml-LOITy_I1.js} +1 -1
- package/dist/cli.js +2586 -1603
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-MYHNxJks.js +0 -1
- package/dist/assets/web/dist/assets/CostsPage-DKk9OjB0.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-Kz6js49i.js +0 -1
- package/dist/assets/web/dist/assets/NotFoundPage-BXLrQQzy.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-BdW0k2h3.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CbTd9SiS.js +0 -9
- package/dist/assets/web/dist/assets/RunRoutePage-br0H-8Pz.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-Bnzb15zH.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-NLhjzHAz.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-D-i0uVw-.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DGXukEAH.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-CYMux25J.js +0 -1
- package/dist/assets/web/dist/assets/favicon-CSt-qDvc.js +0 -1
- package/dist/assets/web/dist/assets/main-CFHF2RvQ.css +0 -1
- package/dist/assets/web/dist/assets/main-Dht0in4v.js +0 -28
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// A resident `down` on a reason only a rebuild can escape is rebuilt on that
|
|
2
|
+
// transition, under a budget (docs/reference/specs/resident-repos.md item 36).
|
|
3
|
+
// Pure: the Worker's `goDown` and the watchdog's backstop both call
|
|
4
|
+
// `autoRebuildDecision` over the history row they persist
|
|
5
|
+
// (`resident:autoRebuilds`, ISO instants of the auto-rebuilds this resident
|
|
6
|
+
// has had), so the transition and the backstop cannot disagree.
|
|
7
|
+
|
|
8
|
+
/** The `down` reasons a rebuild is the only exit from: a down resident runs no
|
|
9
|
+
* cycle, and these say the snapshot (or the container that would restore it)
|
|
10
|
+
* cannot be used — never a provision failure, which would loop against the
|
|
11
|
+
* same broken build. `runtime-unreachable` is item 64's last rung: a recreated
|
|
12
|
+
* container that did not answer either. */
|
|
13
|
+
export const REHYDRATION_FAILURE_RE = /^(r2-restore-failed|snapshot-stamp-mismatch|no-snapshot|runtime-unreachable)/;
|
|
14
|
+
|
|
15
|
+
/** How many auto-rebuilds one resident gets inside one window. A rebuild is a
|
|
16
|
+
* clone, an install and a build (minutes to half an hour) that holds the cap
|
|
17
|
+
* slot and writes a fresh snapshot; a resident that comes back `warm` and
|
|
18
|
+
* goes `down` again on the same class of reason is flapping on something a
|
|
19
|
+
* rebuild does not fix, and the third time it stays down where a person can
|
|
20
|
+
* see it. */
|
|
21
|
+
export const AUTO_REBUILD_BUDGET = 2;
|
|
22
|
+
export const AUTO_REBUILD_WINDOW_MS = 24 * 60 * 60_000;
|
|
23
|
+
|
|
24
|
+
/** The stamp a spent budget leaves on the reason — the one mark the decision
|
|
25
|
+
* reads back so the watchdog's passes never judge the same down twice. */
|
|
26
|
+
const SPENT_MARK = " — auto-rebuild budget spent (";
|
|
27
|
+
|
|
28
|
+
export type AutoRebuildDecision =
|
|
29
|
+
/** Rebuild now; `history` is what to persist (pruned, this instant appended); `reason` is the rebuild's. */
|
|
30
|
+
| { action: "rebuild"; history: string[]; reason: string }
|
|
31
|
+
/** Stay down; `reason` is the down reason stamped with the budget; `history` is pruned. */
|
|
32
|
+
| { action: "budget-spent"; history: string[]; reason: string }
|
|
33
|
+
| { action: "not-eligible"; why: "reason" | "already-spent" };
|
|
34
|
+
|
|
35
|
+
/** A `down` reason the transition (or the watchdog) may rebuild from: a
|
|
36
|
+
* rehydration failure that no spent budget has stamped yet. */
|
|
37
|
+
export function isAutoRebuildEligible(reason: string): boolean {
|
|
38
|
+
return REHYDRATION_FAILURE_RE.test(reason) && !reason.includes(SPENT_MARK);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Judge one `down`. `history` are the ISO instants of this resident's past
|
|
42
|
+
* auto-rebuilds; entries outside the window (or unparseable) are dropped
|
|
43
|
+
* before counting and before writing back. */
|
|
44
|
+
export function autoRebuildDecision(input: {
|
|
45
|
+
reason: string;
|
|
46
|
+
history: readonly string[];
|
|
47
|
+
now: number;
|
|
48
|
+
}): AutoRebuildDecision {
|
|
49
|
+
if (!REHYDRATION_FAILURE_RE.test(input.reason)) return { action: "not-eligible", why: "reason" };
|
|
50
|
+
if (input.reason.includes(SPENT_MARK)) return { action: "not-eligible", why: "already-spent" };
|
|
51
|
+
const windowStart = input.now - AUTO_REBUILD_WINDOW_MS;
|
|
52
|
+
const kept = input.history
|
|
53
|
+
.map((iso) => ({ iso, at: Date.parse(iso) }))
|
|
54
|
+
.filter((e) => Number.isFinite(e.at) && e.at > windowStart)
|
|
55
|
+
.sort((a, b) => a.at - b.at);
|
|
56
|
+
const hours = Math.round(AUTO_REBUILD_WINDOW_MS / 3_600_000);
|
|
57
|
+
if (kept.length >= AUTO_REBUILD_BUDGET) {
|
|
58
|
+
// No reopen instant on the stamp: a stamped row is never judged again and
|
|
59
|
+
// a down resident runs no cycle, so nothing resumes when the window
|
|
60
|
+
// passes — only a person's rebuild recovers it, and the stamp says so.
|
|
61
|
+
return {
|
|
62
|
+
action: "budget-spent",
|
|
63
|
+
history: kept.map((e) => e.iso),
|
|
64
|
+
reason: `${input.reason}${SPENT_MARK}${AUTO_REBUILD_BUDGET} in ${hours} h) — repo rebuild resets it`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const history = [...kept.map((e) => e.iso), new Date(input.now).toISOString()];
|
|
68
|
+
return {
|
|
69
|
+
action: "rebuild",
|
|
70
|
+
history,
|
|
71
|
+
reason: `auto-rebuild (${history.length} of ${AUTO_REBUILD_BUDGET} in ${hours} h): ${input.reason}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -180,7 +180,7 @@ export function killStaleBuildProcessesCommand(user: string, dir: string): strin
|
|
|
180
180
|
* a tool naming the signal. A bare "killed" is NOT enough — compilers and
|
|
181
181
|
* OOM messages say it too — and a step the cycle itself timed out is a real
|
|
182
182
|
* failure however it died. */
|
|
183
|
-
const INTERRUPTION_SIGNATURE = /\bexit 143\b|Session terminated|SIGTERM
|
|
183
|
+
const INTERRUPTION_SIGNATURE = /\bexit 143\b|Session terminated|SIGTERM|^restore-interrupted:/;
|
|
184
184
|
|
|
185
185
|
/** Message wording of the Sandbox SDK's runtime-replacement error family — the
|
|
186
186
|
* container went away UNDER a live SDK call, so the failure never reaches the
|
|
@@ -396,12 +396,34 @@ export function isRuntimeUnreachableReason(reason: string): boolean {
|
|
|
396
396
|
* means the stream ran on before the replacement. */
|
|
397
397
|
export function restoreFailureDisposition(
|
|
398
398
|
message: string,
|
|
399
|
-
facts: { runtimeReplaced?: boolean } = {},
|
|
399
|
+
facts: { runtimeReplaced?: boolean; runtimeActive?: boolean } = {},
|
|
400
400
|
): { action: "interrupted"; reason: string } | { action: "down"; reason: string } {
|
|
401
401
|
const timedOut = /\(timed out\)/.test(message);
|
|
402
|
-
if (
|
|
402
|
+
if (timedOut) {
|
|
403
|
+
return {
|
|
404
|
+
action: "down",
|
|
405
|
+
reason: `r2-restore-failed: ${message} — container stopped so the transfer cannot land on a rebuild`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
// The kill signature the instance step's classifier reads
|
|
409
|
+
// (INTERRUPTION_SIGNATURE): a container roll SIGTERMs the extract, whose
|
|
410
|
+
// exec comes back a normal `exit 143` result — the SDK's replacement
|
|
411
|
+
// wording only reaches the execs that follow. Same event, same reading.
|
|
412
|
+
if (
|
|
413
|
+
facts.runtimeReplaced === true ||
|
|
414
|
+
RUNTIME_REPLACEMENT_WORDING.test(message) ||
|
|
415
|
+
INTERRUPTION_SIGNATURE.test(message)
|
|
416
|
+
) {
|
|
403
417
|
return { action: "interrupted", reason: `restore-interrupted: ${message}` };
|
|
404
418
|
}
|
|
419
|
+
// The Worker's probe after the failure: a runtime that is no longer active
|
|
420
|
+
// took the disk the extract wrote to with it, whatever the exit code said.
|
|
421
|
+
if (facts.runtimeActive === false) {
|
|
422
|
+
return {
|
|
423
|
+
action: "interrupted",
|
|
424
|
+
reason: `restore-interrupted: ${message} — the container stopped under the restore`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
405
427
|
return {
|
|
406
428
|
action: "down",
|
|
407
429
|
reason: `r2-restore-failed: ${message} — container stopped so the transfer cannot land on a rebuild`,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** The ledger of held `/await-restore` requests (docs/reference/specs/execution.md
|
|
2
|
+
* item 27): each waiter is one resolver held in memory until the resident's
|
|
3
|
+
* lifecycle state leaves `restoring`, when `publish` answers every held
|
|
4
|
+
* request at once with the state it landed on. Event-driven by construction —
|
|
5
|
+
* no polling and no retry timer anywhere: the one publish is the state
|
|
6
|
+
* transition itself (`setResidentState`). In-DO memory only: a Durable Object
|
|
7
|
+
* restart drops the held RPCs with the waiters, and the bot's own request
|
|
8
|
+
* deadline turns that into a named cold fallback, never a hang. */
|
|
9
|
+
export interface RestoreOutcome {
|
|
10
|
+
/** The lifecycle state the resident landed on when the restore span ended. */
|
|
11
|
+
state: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** One registered waiter: the outcome it will be answered with, and the way
|
|
16
|
+
* to withdraw it when the caller learns it need not wait after all. */
|
|
17
|
+
export interface RestoreHold {
|
|
18
|
+
outcome: Promise<RestoreOutcome>;
|
|
19
|
+
/** Take the waiter off the ledger; a no-op once a publish answered it. */
|
|
20
|
+
withdraw(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class RestoreWaiters {
|
|
24
|
+
private waiters: Array<(outcome: RestoreOutcome) => void> = [];
|
|
25
|
+
|
|
26
|
+
/** How many requests are held right now (the ledger's size, for tests and logs). */
|
|
27
|
+
get size(): number {
|
|
28
|
+
return this.waiters.length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Register a waiter for the next publish. Register BEFORE reading the
|
|
32
|
+
* state you are waiting to leave: a transition that lands between the read
|
|
33
|
+
* and the registration would otherwise publish to a ledger this request is
|
|
34
|
+
* not on yet, and hold it for a transition that may never come. A caller
|
|
35
|
+
* whose read then says the wait is over withdraws. */
|
|
36
|
+
hold(): RestoreHold {
|
|
37
|
+
let resolve!: (outcome: RestoreOutcome) => void;
|
|
38
|
+
const outcome = new Promise<RestoreOutcome>((r) => (resolve = r));
|
|
39
|
+
this.waiters.push(resolve);
|
|
40
|
+
return {
|
|
41
|
+
outcome,
|
|
42
|
+
withdraw: () => {
|
|
43
|
+
const at = this.waiters.indexOf(resolve);
|
|
44
|
+
if (at !== -1) this.waiters.splice(at, 1);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Hold one request until the next publish. */
|
|
50
|
+
wait(): Promise<RestoreOutcome> {
|
|
51
|
+
return this.hold().outcome;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Answer every held request with the state the resident landed on and
|
|
55
|
+
* empty the ledger; a publish with nothing held is a no-op. */
|
|
56
|
+
publish(outcome: RestoreOutcome): void {
|
|
57
|
+
const held = this.waiters;
|
|
58
|
+
this.waiters = [];
|
|
59
|
+
for (const resolve of held) resolve(outcome);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// The seeded sandbox (docs/reference/specs/execution.md item 25). A cold
|
|
2
|
+
// per-thread sandbox clones and installs a repository from nothing — minutes
|
|
3
|
+
// on a large one, every time a resident refuses a run. A resident already
|
|
4
|
+
// holds a stamped snapshot of the same repository in R2: the checkout without
|
|
5
|
+
// its dependency view, and one archive per lockfile key holding the view. The
|
|
6
|
+
// seed restores both into the sandbox BEFORE the run's first command, fixes
|
|
7
|
+
// ownership and origin, moves the view into place and checks the thread's ref
|
|
8
|
+
// out, so the run starts where a resident's would. This module is the plan's
|
|
9
|
+
// pure half — the handle's shape as the bot forwards it from the resident's
|
|
10
|
+
// `/status`, the fix-up script, the classification of a restore whose objects
|
|
11
|
+
// are gone — free of node: imports so wrangler bundles it into the sandbox
|
|
12
|
+
// Worker like sandboxErrors.ts. The Worker runs it; the bot forwards it.
|
|
13
|
+
|
|
14
|
+
import { shellQuote } from "./shellQuote.js";
|
|
15
|
+
|
|
16
|
+
/** Where the seeded checkout lands: the run's working tree. */
|
|
17
|
+
export const SEED_CHECKOUT_DIR = "/workspace/checkout";
|
|
18
|
+
/** Where the deps-store entry's archive is extracted before it becomes the checkout's `node_modules`. */
|
|
19
|
+
export const SEED_DEPS_STAGING_DIR = "/workspace/.seed-deps";
|
|
20
|
+
/** The marker a seeded container carries (its text is `seedMarkerText`): what it was
|
|
21
|
+
* seeded from AND what was checked out, so a second `/seed` for the same seed answers
|
|
22
|
+
* at once — never restoring over a live tree — while one that names another ref or
|
|
23
|
+
* head is a new seed. */
|
|
24
|
+
export const SEED_MARKER = "/workspace/.switchboard-seed";
|
|
25
|
+
|
|
26
|
+
/** The marker's one line: the checkout handle, the ref the tree is on, the head asked
|
|
27
|
+
* for (or `-`). Two seeds are the same seed exactly when these agree; a retry carries
|
|
28
|
+
* the identical seed, a re-attach on another branch does not. */
|
|
29
|
+
export function seedMarkerText(seed: Pick<SandboxSeed, "checkoutBackupId" | "ref" | "fetchRef" | "fetchSha">): string {
|
|
30
|
+
return `${seed.checkoutBackupId} ${seed.fetchRef ?? seed.ref} ${seed.fetchSha ?? "-"}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One cap shared by both restores, judged by bytes arriving (the SDK's
|
|
34
|
+
* restore takes no timeout): the checkout and the deps view together. */
|
|
35
|
+
export const SEED_RESTORE_MAX_MS = 8 * 60_000;
|
|
36
|
+
/** How long a failure sweep waits for a restore the judge gave up on to settle before it
|
|
37
|
+
* removes the restore's directory: the SDK's call cannot be cancelled, and a stalled
|
|
38
|
+
* transfer often finishes soon after the stall window. */
|
|
39
|
+
export const SEED_ABANDONED_RESTORE_WAIT_MS = 60_000;
|
|
40
|
+
/** The fix-up script's own limit: a `chown -R` over a large tree, a fetch, a checkout. */
|
|
41
|
+
export const SEED_FIXUP_TIMEOUT_MS = 3 * 60_000;
|
|
42
|
+
/** The client's per-send budget for `POST /seed`: both caps plus the answer. */
|
|
43
|
+
export const SEED_BUDGET_MS = SEED_RESTORE_MAX_MS + SEED_FIXUP_TIMEOUT_MS + 60_000;
|
|
44
|
+
|
|
45
|
+
/** Why a seed did not happen, as the machine tokens the refusal carries:
|
|
46
|
+
* the handle's objects are gone (the bot re-reads `/status` once and retries),
|
|
47
|
+
* a step failed (the run goes cold with the note), or the Worker has no
|
|
48
|
+
* presigned transfer configured (a gigabyte restore never goes through the
|
|
49
|
+
* isolate — resident-repos.md item 61). */
|
|
50
|
+
export const SEED_REASONS = ["seed-missing", "seed-failed", "seed-unconfigured"] as const;
|
|
51
|
+
export type SeedReason = (typeof SEED_REASONS)[number];
|
|
52
|
+
|
|
53
|
+
/** The handle the bot forwards: the resident's snapshot as `/status` publishes
|
|
54
|
+
* it, plus the thread's own ref and head when they differ from the snapshot's. */
|
|
55
|
+
export interface SandboxSeed {
|
|
56
|
+
/** `owner/name`: where the checkout's origin points after the fix-up. */
|
|
57
|
+
slug: string;
|
|
58
|
+
/** The snapshot's checkout archive (resident-repos.md item 7). */
|
|
59
|
+
checkoutBackupId: string;
|
|
60
|
+
/** The deps-store entry archive for the snapshot's lockfile key, when the resident has one (item 61). */
|
|
61
|
+
depsBackupId?: string;
|
|
62
|
+
/** The snapshot's stamp: the branch the checkout is on and its head. */
|
|
63
|
+
ref: string;
|
|
64
|
+
sha: string;
|
|
65
|
+
/** The thread's ref to fetch and check out; absent → the checkout stays on the snapshot's branch. */
|
|
66
|
+
fetchRef?: string;
|
|
67
|
+
/** The thread's resolved head, preferred over the fetched tip when the fetch already holds it. */
|
|
68
|
+
fetchSha?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type SeedStep = "restore" | "deps" | "fixup";
|
|
72
|
+
|
|
73
|
+
export type SeedAnswer =
|
|
74
|
+
| {
|
|
75
|
+
seeded: true;
|
|
76
|
+
/** The container already carried this handle's tree: nothing was restored. */
|
|
77
|
+
cached: boolean;
|
|
78
|
+
slug: string;
|
|
79
|
+
/** What the checkout is on after the fix-up. */
|
|
80
|
+
ref: string;
|
|
81
|
+
sha: string;
|
|
82
|
+
from: { ref: string; sha: string; checkoutBackupId: string; depsBackupId?: string };
|
|
83
|
+
/** Milliseconds per step; `deps` null when no entry rode along; all zero when cached. */
|
|
84
|
+
steps: { restore: number; deps: number | null; fixup: number };
|
|
85
|
+
ms: number;
|
|
86
|
+
}
|
|
87
|
+
| { seeded: false; reason: SeedReason; detail: string; step?: SeedStep };
|
|
88
|
+
|
|
89
|
+
/** A backup id as the SDK mints it (a UUID: hex and hyphens) — it becomes a
|
|
90
|
+
* path and a glob on the container, so nothing else may. */
|
|
91
|
+
const BACKUP_ID_RE = /^(?=.*[0-9a-fA-F])[0-9a-fA-F-]{8,64}$/;
|
|
92
|
+
const SLUG_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
93
|
+
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
94
|
+
/** A branch name git accepts (`check-ref-format --branch`, in the shape a
|
|
95
|
+
* Slack thread binds): printable, no leading `-`/`/`/`.`, no `..`, no
|
|
96
|
+
* `//`, none of git's reserved characters, no trailing `.` or `.lock`. */
|
|
97
|
+
const REF_RE = /^(?![-/.])(?!.*\.\.)(?!.*\/\/)(?!.*\.lock$)(?!.*\.$)(?!.*@\{)[\x21-\x7e]{1,255}$/;
|
|
98
|
+
const REF_FORBIDDEN = /[~^:?*[\\]/;
|
|
99
|
+
|
|
100
|
+
const isRef = (v: unknown): v is string => typeof v === "string" && REF_RE.test(v) && !REF_FORBIDDEN.test(v);
|
|
101
|
+
const isId = (v: unknown): v is string => typeof v === "string" && BACKUP_ID_RE.test(v);
|
|
102
|
+
const isSha = (v: unknown): v is string => typeof v === "string" && SHA_RE.test(v);
|
|
103
|
+
|
|
104
|
+
/** The body's `seed`, checked field by field; every refusal names the field. */
|
|
105
|
+
export function parseSeed(v: unknown): { ok: true; seed: SandboxSeed } | { ok: false; error: string } {
|
|
106
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return { ok: false, error: "seed: not an object" };
|
|
107
|
+
const o = v as Record<string, unknown>;
|
|
108
|
+
const fail = (error: string) => ({ ok: false as const, error: `seed: ${error}` });
|
|
109
|
+
if (!isId(o.checkoutBackupId)) return fail("checkoutBackupId is not a backup id");
|
|
110
|
+
if (o.depsBackupId !== undefined && !isId(o.depsBackupId)) return fail("depsBackupId is not a backup id");
|
|
111
|
+
if (typeof o.slug !== "string" || !SLUG_RE.test(o.slug)) return fail("slug is not owner/name");
|
|
112
|
+
if (!isRef(o.ref)) return fail("ref is not a branch name");
|
|
113
|
+
if (o.fetchRef !== undefined && !isRef(o.fetchRef)) return fail("fetchRef is not a branch name");
|
|
114
|
+
if (!isSha(o.sha)) return fail("sha is not a commit sha");
|
|
115
|
+
if (o.fetchSha !== undefined && !isSha(o.fetchSha)) return fail("fetchSha is not a commit sha");
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
seed: {
|
|
119
|
+
slug: o.slug,
|
|
120
|
+
checkoutBackupId: o.checkoutBackupId,
|
|
121
|
+
...(o.depsBackupId !== undefined ? { depsBackupId: o.depsBackupId } : {}),
|
|
122
|
+
ref: o.ref,
|
|
123
|
+
sha: o.sha,
|
|
124
|
+
...(o.fetchRef !== undefined ? { fetchRef: o.fetchRef } : {}),
|
|
125
|
+
...(o.fetchSha !== undefined ? { fetchSha: o.fetchSha } : {}),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The fix-up after the restores, as root inside the sandbox, failing fast:
|
|
131
|
+
* 1. the tree becomes root's — the archive came from the resident's build
|
|
132
|
+
* user, and a sandbox runs everything as root, so git would otherwise
|
|
133
|
+
* refuse the "dubious ownership" and every write would need a chown;
|
|
134
|
+
* 2. origin points at GitHub — the resident's checkout fetched from its
|
|
135
|
+
* local mirror;
|
|
136
|
+
* 3. the deps view, when one was restored, replaces whatever `node_modules`
|
|
137
|
+
* the checkout carries (older snapshots still hold one);
|
|
138
|
+
* 4. the thread's ref is fetched from origin (the credential is the exec
|
|
139
|
+
* env's `GH_TOKEN`, through the image's `gh` credential helper — never a
|
|
140
|
+
* token in this text) and checked out, at its resolved head when the
|
|
141
|
+
* fetch holds it, else at the fetched tip; without a thread ref the
|
|
142
|
+
* checkout stays on the snapshot's branch;
|
|
143
|
+
* 5. the head is printed last: the answer's `sha`. */
|
|
144
|
+
export function seedFixupScript(input: {
|
|
145
|
+
slug: string;
|
|
146
|
+
ref: string;
|
|
147
|
+
fetchRef?: string;
|
|
148
|
+
fetchSha?: string;
|
|
149
|
+
checkoutDir: string;
|
|
150
|
+
depsDir?: string;
|
|
151
|
+
}): string {
|
|
152
|
+
const lines = [
|
|
153
|
+
"set -e",
|
|
154
|
+
`cd ${shellQuote(input.checkoutDir)}`,
|
|
155
|
+
"chown -R 0:0 .",
|
|
156
|
+
`git remote set-url origin ${shellQuote(`https://github.com/${input.slug}.git`)}`,
|
|
157
|
+
];
|
|
158
|
+
if (input.depsDir) {
|
|
159
|
+
lines.push("rm -rf node_modules", `mv ${shellQuote(input.depsDir)} node_modules`);
|
|
160
|
+
}
|
|
161
|
+
if (input.fetchRef) {
|
|
162
|
+
const ref = shellQuote(input.fetchRef);
|
|
163
|
+
lines.push(
|
|
164
|
+
`git fetch --no-tags origin ${shellQuote(`+refs/heads/${input.fetchRef}:refs/remotes/origin/${input.fetchRef}`)}`,
|
|
165
|
+
);
|
|
166
|
+
if (input.fetchSha) {
|
|
167
|
+
const sha = shellQuote(input.fetchSha);
|
|
168
|
+
lines.push(
|
|
169
|
+
`if git cat-file -e ${sha}'^{commit}' 2>/dev/null; then git checkout -q -B ${ref} ${sha}; else git checkout -q -B ${ref} ${shellQuote(`origin/${input.fetchRef}`)}; fi`,
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
lines.push(`git checkout -q -B ${ref} ${shellQuote(`origin/${input.fetchRef}`)}`);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
lines.push(`git checkout -q -B ${shellQuote(input.ref)}`);
|
|
176
|
+
}
|
|
177
|
+
lines.push("git rev-parse HEAD");
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The SDK's missing-backup error — the handle's objects are gone (a rotation
|
|
182
|
+
* or an offboard took them): by name, which survives the RPC boundary, or by
|
|
183
|
+
* either of its two texts. */
|
|
184
|
+
export function isBackupMissing(shape: { name?: string; message?: string }): boolean {
|
|
185
|
+
if (shape.name === "BackupNotFoundError") return true;
|
|
186
|
+
return /Backup (?:archive )?not found/i.test(shape.message ?? "");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// -- the bot's half: from the resident's handle to a selection -----------------
|
|
190
|
+
|
|
191
|
+
/** The seed handle as the resident's `/status` publishes it (the bot's probe
|
|
192
|
+
* carries it as `seed`, [resident.ts](./resident.ts)). */
|
|
193
|
+
export interface SeedHandle {
|
|
194
|
+
checkoutBackupId: string;
|
|
195
|
+
depsBackupId?: string;
|
|
196
|
+
ref: string;
|
|
197
|
+
sha: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The seed for one thread: the resident's handle plus the thread's own ref
|
|
201
|
+
* and resolved head when it has them — a thread bound to a branch is checked
|
|
202
|
+
* out on it, a thread with none stays on the snapshot's. */
|
|
203
|
+
export function seedForThread(
|
|
204
|
+
handle: SeedHandle,
|
|
205
|
+
thread: { slug: string; ref?: string; headSha?: string },
|
|
206
|
+
): SandboxSeed {
|
|
207
|
+
return {
|
|
208
|
+
slug: thread.slug,
|
|
209
|
+
checkoutBackupId: handle.checkoutBackupId,
|
|
210
|
+
...(handle.depsBackupId ? { depsBackupId: handle.depsBackupId } : {}),
|
|
211
|
+
ref: handle.ref,
|
|
212
|
+
sha: handle.sha,
|
|
213
|
+
...(thread.ref ? { fetchRef: thread.ref } : {}),
|
|
214
|
+
...(thread.ref && thread.headSha ? { fetchSha: thread.headSha } : {}),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** What a seeded sandbox is, on the selection: where the checkout is and what
|
|
219
|
+
* it is on, for the prompt and the card. */
|
|
220
|
+
export interface SeededSandbox {
|
|
221
|
+
slug: string;
|
|
222
|
+
ref: string;
|
|
223
|
+
sha: string;
|
|
224
|
+
/** The checkout's path inside the sandbox — the run's working tree. */
|
|
225
|
+
workspace: string;
|
|
226
|
+
/** The container already carried this seed: nothing was restored. */
|
|
227
|
+
cached: boolean;
|
|
228
|
+
ms: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** After a refused seed: retry once with a fresh handle when the handle's
|
|
232
|
+
* objects were gone and the resident has since published another (a rotation
|
|
233
|
+
* took the first); otherwise the run goes cold, and the reason says why. */
|
|
234
|
+
export type SeedRetry = { action: "retry"; seed: SandboxSeed } | { action: "cold"; why: string };
|
|
235
|
+
|
|
236
|
+
export function seedRetryDecision(input: {
|
|
237
|
+
answer: Extract<SeedAnswer, { seeded: false }>;
|
|
238
|
+
attempted: SandboxSeed;
|
|
239
|
+
fresh: SeedHandle | undefined;
|
|
240
|
+
alreadyRetried: boolean;
|
|
241
|
+
}): SeedRetry {
|
|
242
|
+
const { answer, attempted, fresh } = input;
|
|
243
|
+
if (answer.reason === "seed-missing" && !input.alreadyRetried) {
|
|
244
|
+
if (fresh && fresh.checkoutBackupId !== attempted.checkoutBackupId) {
|
|
245
|
+
return {
|
|
246
|
+
action: "retry",
|
|
247
|
+
seed: seedForThread(fresh, {
|
|
248
|
+
slug: attempted.slug,
|
|
249
|
+
...(attempted.fetchRef ? { ref: attempted.fetchRef } : {}),
|
|
250
|
+
...(attempted.fetchSha ? { headSha: attempted.fetchSha } : {}),
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return { action: "cold", why: `seed missing (${answer.detail}) and the resident published no newer handle` };
|
|
255
|
+
}
|
|
256
|
+
return { action: "cold", why: `${answer.reason.replace("seed-", "seed ")} (${answer.detail})` };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The card's one line for a seeded sandbox, after the reason the resident
|
|
260
|
+
* was not used: what it was seeded from, so a reader tells the three paths
|
|
261
|
+
* — resident, seeded, cold — apart from Slack alone. */
|
|
262
|
+
export function seededSandboxNote(
|
|
263
|
+
reason: string,
|
|
264
|
+
seeded: Pick<SeededSandbox, "slug" | "ref" | "sha" | "cached">,
|
|
265
|
+
): string {
|
|
266
|
+
return `${reason} — seeded sandbox · from resident snapshot${seeded.cached ? " (already seeded)" : ""} · ${seeded.slug} · ${seeded.ref}@${seeded.sha.slice(0, 7)}`;
|
|
267
|
+
}
|
|
@@ -234,6 +234,13 @@ export interface McpServerView {
|
|
|
234
234
|
addedByName?: string;
|
|
235
235
|
addedAt?: number;
|
|
236
236
|
promotedFrom?: string;
|
|
237
|
+
/** `promotedFrom` as a display name, when the lookup answered. */
|
|
238
|
+
promotedFromName?: string;
|
|
239
|
+
/** A user row's owner (the id after `user:` in `scopeKey`) as a display name, when the lookup answered. */
|
|
240
|
+
ownerName?: string;
|
|
241
|
+
/** A higher tier holds this name, so runs use that entry and this one is idle (`mcpServersFor`'s
|
|
242
|
+
* `shadowedBy`, the winning scope key — `org`, or `channel:<id>`); absent when this entry serves. */
|
|
243
|
+
shadowedBy?: string;
|
|
237
244
|
}
|
|
238
245
|
|
|
239
246
|
export function serverView(
|