@coreplane/switchboard 1.236.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/deploy/cloudflare-resident/worker.ts +175 -62
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +36 -10
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -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/driver.ts +5 -1
- package/dist/assets/src/core/runEvents.ts +10 -6
- package/dist/assets/src/core/ship/coordinator.ts +23 -1
- package/dist/assets/src/execution/residentDepsStore.ts +14 -2
- 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/sandboxLifecycle.ts +33 -0
- package/dist/assets/web/dist/.vite/manifest.json +408 -408
- package/dist/assets/web/dist/assets/AppShell-MYHNxJks.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-BfJsol9y.js → CostsPage-DKk9OjB0.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-CvInfu7y.js → DeliveryPage-Kz6js49i.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-d37EP8hS.js → NotFoundPage-BXLrQQzy.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DQSD0SZD.js → ResidentDetailPage-Dc23v_8v.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-TR5D1TRR.js +1 -0
- package/dist/assets/web/dist/assets/{RunFoldRow-Bngn4GjF.js → RunFoldRow-mkUj7WEN.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-a-tMrirB.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-CyCdOhJ2.js → RunsTabs-XpCqeWu4.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-ByCmyrDs.js → ScheduledPage-CdCVyNbh.js} +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-DuA8JpLU.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-D-BPSPpw.js → StatusDot-DmNHX6am.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DPDx5tXT.js → Tooltip-qVnJTgSt.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-0eiekTHQ.js → UnitRoutePage-BcTeH7m2.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-Ni1Xil2G.js → angular-html-DYtV9uIN.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-oCgo6p05.js → angular-ts-DF2P25Zp.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CiWYl0gh.js → apl-Ff96wxko.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-B-VeKZ_J.js → astro-LbBnZna-.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-BIWEISKd.js → blade-DltuxeBW.js} +1 -1
- package/dist/assets/web/dist/assets/{c-DHxA5Byp.js → c-DBg7yuPx.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-D4qGLw-Q.js → chapel-C83hUAds.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BznX5Q-u.js → cobol-DB0yV_2D.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-CVr8Idlj.js → coffee-Bpih6Kzl.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-wrk6NfW4.js → cpp-CU00ZeRn.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CGYWMeH1.js → crystal-f8ImJkux.js} +1 -1
- package/dist/assets/web/dist/assets/{css-BgGytMsy.js → css-4VrC4ISo.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-DSkVB16z.js → dist-TAGawIfM.js} +2 -2
- package/dist/assets/web/dist/assets/{durationTone-DYogEPEk.js → durationTone-CYMux25J.js} +1 -1
- package/dist/assets/web/dist/assets/{edge-CdR6AU9v.js → edge-BwFDzfS_.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-CQRb3PZx.js → elixir-wwIYIpJm.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-CpuA6W8B.js → elm-DyDuM3v3.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CvlrblqH.js → erb-DFJBEVrh.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-COnovCVc.js → git-rebase-CuDv-44X.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-C-DS4g_J.js → glimmer-js-ZFnrm4J3.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-DTkorWRI.js → glimmer-ts-XY4Yqshp.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-CH51-JW7.js → glsl-xnR8efx0.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-BO-XymNv.js → graphql-D-gO5tnP.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-C-N0oicf.js → hack-CSxbW4ax.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-BfmdtWoP.js → haml-D4ar42QT.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-BqhO_te-.js → handlebars-CMLbDE7o.js} +1 -1
- package/dist/assets/web/dist/assets/{html-DUEZlTKV.js → html-CVekU4Fs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DOUJTS8A.js → html-derivative-St3HHw_N.js} +1 -1
- package/dist/assets/web/dist/assets/{http-j7UH0Cqv.js → http-Mon0DWHS.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-D6ZaBLJk.js → hurl-Cbl5IvBs.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-BjXBzI5Z.js → indexRow-FLJu5mR9.js} +1 -1
- package/dist/assets/web/dist/assets/{java-dugWx3wi.js → java-Bprck1l-.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CK4GC4JO.js → javascript-C73G1-UR.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-DrLKIyKG.js → jinja-C4DuRxYJ.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-CS1Th-TO.js → jison-B2U03GEC.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CkBqdVys.js → json-CvTZUDzx.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CCxdg7n3.js → jsx-DMWKCzwo.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-BalKW2hA.js → julia-Czn7bV_3.js} +1 -1
- package/dist/assets/web/dist/assets/{just-BKw1G40D.js → just-BBxpNfvC.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-Dfad6ZN-.js → latex-Bw9yg5mr.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-f_CMGBvd.js → liquid-BRCjaydE.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-BcMK_uKM.js → lua-DUu2KIC0.js} +1 -1
- package/dist/assets/web/dist/assets/main-csihqEj5.css +1 -0
- package/dist/assets/web/dist/assets/{main-DR8iKpNt.js → main-mUd_TJjH.js} +2 -2
- package/dist/assets/web/dist/assets/{marko-B0oNLx2i.js → marko-CZJZIlSQ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-Bc_MmIyq.js → mdc-BmClq6fd.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Cmxv8RIt.js → nginx-Bmt3xSEm.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BtgRfaY0.js → nim-Da-i0utK.js} +1 -1
- package/dist/assets/web/dist/assets/{org-Dl9EzlHh.js → org-BvAre2qP.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-BmF7x7BB.js → perl-v9WwFFtB.js} +1 -1
- package/dist/assets/web/dist/assets/{php-rH6CVptQ.js → php-DTU4Zyze.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-CIN5Ccck.js → pug-DhTwEqTO.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BpLS3RFr.js → qml-BUD9AC_c.js} +1 -1
- package/dist/assets/web/dist/assets/{r-BUMF3B-H.js → r-CAnu56n7.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-B7sTBPdT.js → razor-Dn0P4gAq.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-h2SvwrsJ.js → regexp-CZyBRaVW.js} +1 -1
- package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +1 -0
- package/dist/assets/web/dist/assets/{rst-DLOWEsD2.js → rst-Ckyd5UsY.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-Cl_-I4k3.js → ruby-BBQ62fsx.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-Cm4M-BvV.js → sas-CW35hMxG.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-CEFCEQUF.js → scss-CpV8pF4J.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CE6zb5eS.js → shellscript-CRJvsmE1.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-B4fDVzrM.js → shellsession-CyriUZxf.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-DwXCMpjE.js → soy-D5QzYUeg.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-DrnAKnyD.js → sql-CUBf_Mse.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-BzlwPWD6.js → stata-B-4n4ITz.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CeUieZXd.js → surrealql-RuWZBLtr.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-BERIpIKR.js → svelte-CO5YzCsK.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-6FSuUDwg.js → templ-DbkWVaBF.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-SWCyMwNX.js → tex-IuxoEcId.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-VWj7gWgY.js → ts-tags-g6dGQPjd.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-B6dzfL8L.js → tsx-WnXwiyH2.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-BDXsr482.js → twig-Lx1G7PnY.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-s100Dt8y.js → typescript-BQd7biLF.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-WfyEAUdt.js → typst-D2lXPHK7.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CkXOOJ-6.js → vue-4wVQD9y9.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-CEpzdcau.js → vue-html-DZPaRte4.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-D9dsfiFA.js → vue-vine-DbmZCmu-.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-iBDofbJq.js → xml-DV16szyo.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-CqbJF4hD.js → xsl-eo5-1sk0.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-D9i6OxYe.js → yaml-BXdX8HZf.js} +1 -1
- package/dist/cli.js +960 -520
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-DZdz_sl7.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CZUGLen6.js +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-s0VKmRda.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-DhKEokGK.js +0 -1
- package/dist/assets/web/dist/assets/main-B77vAZVg.css +0 -1
- package/dist/assets/web/dist/assets/residentsModel-VSJEepL1.js +0 -1
|
@@ -47,6 +47,16 @@ export interface Actor {
|
|
|
47
47
|
readonly onBehalfOf?: Actor;
|
|
48
48
|
/** Where a chat actor is speaking from — context, never authority. */
|
|
49
49
|
readonly origin?: { readonly channelId: string; readonly threadKey: string };
|
|
50
|
+
/**
|
|
51
|
+
* The ids that mean "me" for this actor (record 0042): its own `id` first,
|
|
52
|
+
* then the person it is linked to — a dashboard session whose Access email
|
|
53
|
+
* names one Slack user carries that `slack:U…` id here. Read by `is-self`,
|
|
54
|
+
* `acts-as-person`, the `me` tier and the "mine" filters; NEVER by a grant
|
|
55
|
+
* check — the link is identity, not authority. Absent → `[id]`.
|
|
56
|
+
*/
|
|
57
|
+
readonly self?: readonly string[];
|
|
58
|
+
/** The linked person, for display and the audit line (`asUser`); absent when unlinked. */
|
|
59
|
+
readonly asUser?: { readonly id: string; readonly name?: string };
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
/** `<group>:<read|write|exec>` plus the non-command actions. A plain
|
|
@@ -103,8 +113,12 @@ export type Condition =
|
|
|
103
113
|
* resource's channel is `public` (a run's stamped `channelVisibility`;
|
|
104
114
|
* `unknown` is never public). One definition for both evaluators. */
|
|
105
115
|
| { readonly kind: "member-of" }
|
|
106
|
-
/** resource.userId
|
|
116
|
+
/** resource.userId is one of the principal's `self` ids (its own id, or the person it is linked to). */
|
|
107
117
|
| { readonly kind: "is-self" }
|
|
118
|
+
/** The principal's `self` names a chat identity (`slack:U…`): every chat
|
|
119
|
+
* actor, and a dashboard session linked to its person (record 0042) — never
|
|
120
|
+
* an unlinked browser session or a credential. */
|
|
121
|
+
| { readonly kind: "acts-as-person" }
|
|
108
122
|
/** actor.grants.repos contains the resource's repo (or is "all"). */
|
|
109
123
|
| { readonly kind: "owner-of" }
|
|
110
124
|
/** actor.grants.channels === "all". */
|
|
@@ -314,7 +314,11 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
function mergeReturn(step: string, a: BotAnswer): StepReturn {
|
|
317
|
-
const { ok, outcome, sha, reason, at } = a.body;
|
|
317
|
+
const { ok, outcome, by, sha, mergedAt, reason, at } = a.body;
|
|
318
|
+
// The door found the pull request already merged after the approval: the
|
|
319
|
+
// merge commit and the time ride the answer, and the unit ends `by: other`.
|
|
320
|
+
if (ok === true && outcome === "merged" && by === "other" && typeof sha === "string" && typeof mergedAt === "string")
|
|
321
|
+
return { type: "merge", step, outcome: "merged", by: "other", sha, mergedAt, at };
|
|
318
322
|
if (ok === true && outcome === "merged" && typeof sha === "string")
|
|
319
323
|
return { type: "merge", step, outcome: "merged", sha, at };
|
|
320
324
|
if (ok === true && (outcome === "pending" || outcome === "refused") && typeof reason === "string")
|
|
@@ -76,13 +76,17 @@ export type RunNoteKind =
|
|
|
76
76
|
* wait had refused; on pi the container is provisioned before pi starts, so
|
|
77
77
|
* the note is a record fact from before the loop's deletion. */
|
|
78
78
|
| "fleet_busy"
|
|
79
|
-
/** The container the run's
|
|
79
|
+
/** The container the run's process ran in was replaced under the live run
|
|
80
80
|
* (docs/reference/specs/harness-pi.md item 16; the resident's roll,
|
|
81
|
-
* resident-repos.md item 65)
|
|
82
|
-
* the restart note, the summary
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
81
|
+
* resident-repos.md item 65). The harness's note is the verdict: the call
|
|
82
|
+
* in flight settled with the restart note, the summary naming both
|
|
83
|
+
* containers. The run loop then decides between two outcomes (harness.md
|
|
84
|
+
* item 6): a relaunch in the replacement, said in a `resumed` note, or —
|
|
85
|
+
* the relaunch refused by name, the ceiling among them — a second note of
|
|
86
|
+
* this kind saying why and that the run closes `interrupted` for a restart
|
|
87
|
+
* from its request. On a record from before the native loop's deletion the
|
|
88
|
+
* note says that loop's settlement instead: the executor waited for the
|
|
89
|
+
* wake and the run went on. */
|
|
86
90
|
| "sandbox_restarted"
|
|
87
91
|
| "stop_requested"
|
|
88
92
|
| "stopped"
|
|
@@ -145,6 +145,14 @@ export function unitBranch(planId: string, slug: string): string {
|
|
|
145
145
|
return `plan/${planId}/${slug}`;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** Whether `ref` is a unit branch of ship's own — `plan/<plan-id>/<unit-slug>`.
|
|
149
|
+
* A thread keeps the binding its last run opened a pull request on, so after
|
|
150
|
+
* a plan's unit it sits at that unit branch; a fresh task there must not take
|
|
151
|
+
* it as the base (the pull request would target the earlier unit). */
|
|
152
|
+
export function isUnitBranch(ref: string): boolean {
|
|
153
|
+
return /^plan\/[^/]+\/[^/]+$/.test(ref);
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
/** The plan and unit a head branch names, or undefined for any other branch — the merge grant's line. */
|
|
149
157
|
export function parsePlanBranch(branch: string): { planId: string; unitSlug: string } | undefined {
|
|
150
158
|
const m = PLAN_BRANCH.exec(branch);
|
|
@@ -496,6 +504,9 @@ export type StepReturn =
|
|
|
496
504
|
| { type: "read-record"; step: string; run: ChildFacts; at: number }
|
|
497
505
|
| { type: "pr-check"; step: string; pr: PrCheck; at: number }
|
|
498
506
|
| { type: "merge"; step: string; outcome: "merged"; sha: string; at: number }
|
|
507
|
+
// The door found the pull request already merged after the approval — auto-merge
|
|
508
|
+
// fired, or a person merged — so the runner merged nothing (`by: other`).
|
|
509
|
+
| { type: "merge"; step: string; outcome: "merged"; by: "other"; sha: string; mergedAt: string; at: number }
|
|
499
510
|
| { type: "merge"; step: string; outcome: "pending" | "refused"; reason: string; at: number }
|
|
500
511
|
| { type: "sleep"; step: string };
|
|
501
512
|
|
|
@@ -1260,7 +1271,18 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1260
1271
|
case "merge": {
|
|
1261
1272
|
const r = ret as Extract<StepReturn, { type: "merge" }>;
|
|
1262
1273
|
if (r.outcome === "merged")
|
|
1263
|
-
|
|
1274
|
+
// Found already merged at the door — auto-merge or a person, after the
|
|
1275
|
+
// approval: the unit is done, the runner merged nothing (`by: other`).
|
|
1276
|
+
return "by" in r
|
|
1277
|
+
? end(clocked, {
|
|
1278
|
+
kind: "merged",
|
|
1279
|
+
by: "other",
|
|
1280
|
+
pr: p.pr,
|
|
1281
|
+
sha: r.sha,
|
|
1282
|
+
mergedAt: r.mergedAt,
|
|
1283
|
+
reviewRounds: s.reviewRounds,
|
|
1284
|
+
})
|
|
1285
|
+
: end(clocked, { kind: "merged", by: "runner", pr: p.pr, sha: r.sha, reviewRounds: s.reviewRounds });
|
|
1264
1286
|
if (r.outcome === "refused")
|
|
1265
1287
|
return end(clocked, { kind: "merge_refused", pr: p.pr, reason: r.reason, reviewRounds: s.reviewRounds });
|
|
1266
1288
|
const waited = r.at - p.since;
|
|
@@ -299,6 +299,14 @@ export function parseDepsStoreListing(stdout: string): DepsStoreListing {
|
|
|
299
299
|
* attaches. Raise with the instance, never by feel. */
|
|
300
300
|
export const DEPS_STORE_MAX_UNREFERENCED = 1;
|
|
301
301
|
|
|
302
|
+
/** How many spares the attach's disk-pressure path keeps: none. A spare is a
|
|
303
|
+
* warm cache for a lockfile key no live tree references, worth minutes on
|
|
304
|
+
* the next attach at that key; a tree refused for want of space is a run
|
|
305
|
+
* falling to a cold sandbox now. Under pressure the spare goes first, before
|
|
306
|
+
* any idle tree (docs/reference/specs/resident-repos.md item 55), and its
|
|
307
|
+
* entry backup with it — the next attach at that key restores or installs. */
|
|
308
|
+
export const DEPS_STORE_MAX_UNREFERENCED_UNDER_PRESSURE = 0;
|
|
309
|
+
|
|
302
310
|
/** Eviction candidates coldest first. Never a protected key (the checkout's,
|
|
303
311
|
* a live binding's, an install in flight). Debris — an incomplete entry
|
|
304
312
|
* with nothing in flight for it — is always first: it is half an install
|
|
@@ -318,6 +326,9 @@ export interface DepsEvictionPlan {
|
|
|
318
326
|
remove: string[];
|
|
319
327
|
/** Keys that stay, for the log. */
|
|
320
328
|
keep: string[];
|
|
329
|
+
/** The entries `remove` names (debris first, then the coldest spares), with
|
|
330
|
+
* their measured sizes — what a pressure eviction gives back. */
|
|
331
|
+
evicted: DepsStoreEntry[];
|
|
321
332
|
}
|
|
322
333
|
|
|
323
334
|
export function planDepsEviction(input: {
|
|
@@ -336,7 +347,8 @@ export function planDepsEviction(input: {
|
|
|
336
347
|
const evictSpares = spares.slice(0, Math.max(0, spares.length - max));
|
|
337
348
|
const remove = [...debris, ...evictSpares].map((e) => depsEntryPath(e.key, storeDir));
|
|
338
349
|
remove.push(...input.leftovers);
|
|
339
|
-
const
|
|
350
|
+
const evicted = [...debris, ...evictSpares];
|
|
351
|
+
const removed = new Set(evicted.map((e) => e.key));
|
|
340
352
|
const keep = input.entries.filter((e) => !removed.has(e.key)).map((e) => e.key);
|
|
341
|
-
return { remove, keep };
|
|
353
|
+
return { remove, keep, evicted };
|
|
342
354
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|