@coreplane/switchboard 1.236.0 → 1.238.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 +210 -74
- 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/src/execution/snapshotRetention.ts +62 -0
- package/dist/assets/src/mcp/registry.ts +9 -1
- 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-CrfCd8fx.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-BdW0k2h3.js +1 -0
- package/dist/assets/web/dist/assets/{RunFoldRow-Bngn4GjF.js → RunFoldRow-CbTd9SiS.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-a-tMrirB.js → RunRoutePage-br0H-8Pz.js} +3 -3
- package/dist/assets/web/dist/assets/RunsIndexPage-Bnzb15zH.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-NLhjzHAz.js} +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-D-i0uVw-.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-D-BPSPpw.js → StatusDot-DkXALvKf.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DPDx5tXT.js → Tooltip-DtLIyi5P.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-0eiekTHQ.js → UnitRoutePage-DGXukEAH.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-DtA7JiH5.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-CFHF2RvQ.css +1 -0
- package/dist/assets/web/dist/assets/{main-DR8iKpNt.js → main-Dht0in4v.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 +1143 -542
- 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
|
@@ -227,6 +227,7 @@ import {
|
|
|
227
227
|
type FetchRecord,
|
|
228
228
|
type SnapshotStamp,
|
|
229
229
|
} from "../../src/execution/residentStepPlan.js";
|
|
230
|
+
import { backupIdsOf, RETIRED_SNAPSHOT_KEY, rotateSnapshots } from "../../src/execution/snapshotRetention.js";
|
|
230
231
|
import {
|
|
231
232
|
DF_FREE_ARGV,
|
|
232
233
|
DISK_FULL_FREE_KIB,
|
|
@@ -252,7 +253,13 @@ import {
|
|
|
252
253
|
type DiskSample,
|
|
253
254
|
type ThreadCostKind,
|
|
254
255
|
} from "../../src/execution/residentDiskBudget.js";
|
|
255
|
-
import {
|
|
256
|
+
import {
|
|
257
|
+
attachTarget,
|
|
258
|
+
type FetchReason,
|
|
259
|
+
mirrorFetchReason,
|
|
260
|
+
parseWantSha,
|
|
261
|
+
wantShaForBinding,
|
|
262
|
+
} from "../../src/execution/residentHead.js";
|
|
256
263
|
import { residentText, sanitizeResidentBody } from "../../src/execution/residentText.js";
|
|
257
264
|
import {
|
|
258
265
|
abandonedWaitStepResult,
|
|
@@ -297,6 +304,9 @@ import {
|
|
|
297
304
|
parseDepsStoreListing,
|
|
298
305
|
planDepsEviction,
|
|
299
306
|
planDepsMaterialization,
|
|
307
|
+
DEPS_STORE_MAX_UNREFERENCED_UNDER_PRESSURE,
|
|
308
|
+
type DepsEvictionPlan,
|
|
309
|
+
type DepsStoreListing,
|
|
300
310
|
} from "../../src/execution/residentDepsStore.js";
|
|
301
311
|
import { buildId, injectedBuildStamp } from "../../src/deploy/buildStamp.js";
|
|
302
312
|
import { createRefreshInstance, createRefreshInstanceNow, type RefreshInstanceParams } from "./refresh";
|
|
@@ -1002,10 +1012,11 @@ interface ResidentRecord {
|
|
|
1002
1012
|
interface ThreadBinding {
|
|
1003
1013
|
threadKey: string;
|
|
1004
1014
|
ref: string;
|
|
1005
|
-
/** How `ref` was chosen
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1015
|
+
/** How `ref` was chosen: the repo default for want of a named branch — when
|
|
1016
|
+
* the binding was made (item 30), or when a return put it there because the
|
|
1017
|
+
* branch it was on is gone (`returned`) — or a branch someone named. Absent
|
|
1018
|
+
* on a binding made before the field: read as `default` iff `ref` is the
|
|
1019
|
+
* default branch. Only a `default` binding may ever move onto a branch. */
|
|
1009
1020
|
boundBy?: BoundBy;
|
|
1010
1021
|
/** The move a binding may make (item 16): from the default it was bound to,
|
|
1011
1022
|
* onto the branch the thread's own run opened a pull request on. Set, the
|
|
@@ -1013,6 +1024,12 @@ interface ThreadBinding {
|
|
|
1013
1024
|
* branch was gone from the mirror and the binding went back to the
|
|
1014
1025
|
* default), and then for the next own pull request. */
|
|
1015
1026
|
rebound?: Rebound;
|
|
1027
|
+
/** The binding's last move back to the default (item 16's second movement):
|
|
1028
|
+
* the branch it was on — one a rebind moved it onto, or one this thread's
|
|
1029
|
+
* own runs pushed (`ownBranches`) and bound by name, a ship unit's — was
|
|
1030
|
+
* gone from the mirror at an attach. From where, to where, which pull
|
|
1031
|
+
* request's branch it was, when. */
|
|
1032
|
+
returned?: Returned;
|
|
1016
1033
|
/** The branches the thread's own runs pushed and the pull requests they
|
|
1017
1034
|
* head, as each run's release handed them over (`/detach` `pushed`, item
|
|
1018
1035
|
* 16a) — written before any eviction decision, so the fact outlives the
|
|
@@ -1122,9 +1139,10 @@ interface AttachOk {
|
|
|
1122
1139
|
* and why — so the bot can say where the follow-up runs and why. */
|
|
1123
1140
|
rebindRefused?: RebindRefused;
|
|
1124
1141
|
/** This attach moved the binding BACK to the default branch (item 16's
|
|
1125
|
-
* second movement): the branch
|
|
1126
|
-
* mirror, so the tree was provisioned on
|
|
1127
|
-
* failing `unknown-ref`. From where, to
|
|
1142
|
+
* second movement): the branch it was on — a rebind's, or one this thread
|
|
1143
|
+
* itself pushed — is gone from the mirror, so the tree was provisioned on
|
|
1144
|
+
* the default instead of the attach failing `unknown-ref`. From where, to
|
|
1145
|
+
* where, which PR's branch it was. */
|
|
1128
1146
|
returned?: Returned;
|
|
1129
1147
|
}
|
|
1130
1148
|
|
|
@@ -2094,13 +2112,20 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2094
2112
|
throw new StepError("cat-file", `git cat-file exited ${r.exitCode}: ${r.stderr.trim() || "(no output)"}`);
|
|
2095
2113
|
}
|
|
2096
2114
|
|
|
2097
|
-
/** Attach's fetch decision (item 51) over the mirror's actual state
|
|
2098
|
-
* is
|
|
2099
|
-
*
|
|
2100
|
-
|
|
2115
|
+
/** Attach's fetch decision (item 51) over the mirror's actual state — why
|
|
2116
|
+
* the mirror is fetched before the tree is cloned, or null: the ref is
|
|
2117
|
+
* missing, `wantSha` names a commit the ref's tip is not at, or the ref is
|
|
2118
|
+
* one the binding could return from (item 16's second movement) and is
|
|
2119
|
+
* verified against the origin before the mirror's word on it is trusted.
|
|
2120
|
+
* The decision itself is the pure, tested `mirrorFetchReason`. */
|
|
2121
|
+
private async mirrorFetchReasonFor(
|
|
2122
|
+
ref: string,
|
|
2123
|
+
wantSha: string | null,
|
|
2124
|
+
returnable: boolean,
|
|
2125
|
+
): Promise<FetchReason | null> {
|
|
2101
2126
|
const refExists = await this.refExists(ref);
|
|
2102
2127
|
const mirrorSha = refExists && wantSha !== null ? await this.readMirrorSha(ref) : undefined;
|
|
2103
|
-
return
|
|
2128
|
+
return mirrorFetchReason({ refExists, mirrorSha, wantSha, returnable });
|
|
2104
2129
|
}
|
|
2105
2130
|
|
|
2106
2131
|
/** Dependency/build cache key: sha256 over the ls-tree lines (mode,
|
|
@@ -2597,7 +2622,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2597
2622
|
[UPDATED_KEY]: new Date(systemClock()).toISOString(),
|
|
2598
2623
|
[DEADLINE_AT_KEY]: systemClock() + provisioningTimeoutMs,
|
|
2599
2624
|
});
|
|
2600
|
-
await this.ctx.storage.delete([FACTS_KEY, SNAPSHOT_KEY]); // defensive: no stale facts from a past life
|
|
2625
|
+
await this.ctx.storage.delete([FACTS_KEY, SNAPSHOT_KEY, RETIRED_SNAPSHOT_KEY]); // defensive: no stale facts from a past life
|
|
2601
2626
|
try {
|
|
2602
2627
|
this.deleteSchedules(PROVISIONING_CALLBACK);
|
|
2603
2628
|
this.deleteSchedules(PROVISION_RUN_CALLBACK);
|
|
@@ -3139,18 +3164,40 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3139
3164
|
|
|
3140
3165
|
/** The cycle's snapshot phase: the stamped pair to R2 and, when this cycle's
|
|
3141
3166
|
* record stands (not superseded by another writer's), the ready marker and
|
|
3142
|
-
* the
|
|
3167
|
+
* the rotation — the replaced pair retired, the pair retired before it
|
|
3168
|
+
* swept (docs/reference/specs/resident-repos.md item 7: two generations, so a
|
|
3169
|
+
* handle a seeded restore read from `/status` still resolves for a cycle).
|
|
3170
|
+
* Answers whether the record committed. */
|
|
3143
3171
|
private async refreshSnapshot(resource: string, stamp: SnapshotStamp): Promise<boolean> {
|
|
3144
3172
|
const snapped = await this.snapshot({ resource, stamp });
|
|
3145
3173
|
const committed = snapped.done || !snapped.superseded;
|
|
3146
3174
|
if (committed) {
|
|
3147
3175
|
await this.writeDiskMarkers({ ready: stamp.sha });
|
|
3148
|
-
const
|
|
3149
|
-
if (
|
|
3176
|
+
const replaced = !snapped.done && !snapped.superseded ? snapped.previous : undefined;
|
|
3177
|
+
if (replaced) await this.retireSnapshot(replaced);
|
|
3150
3178
|
}
|
|
3151
3179
|
return committed;
|
|
3152
3180
|
}
|
|
3153
3181
|
|
|
3182
|
+
/** The rotation's bookkeeping: the retired generation recorded (the storage
|
|
3183
|
+
* write first, so a deletion that fails leaves the record true and the
|
|
3184
|
+
* 1-year R2 TTL as the backstop), then the objects the rotation frees. */
|
|
3185
|
+
private async retireSnapshot(replaced: SnapshotRecord): Promise<void> {
|
|
3186
|
+
const rotation = rotateSnapshots({ replaced, retired: await this.retiredSnapshot() });
|
|
3187
|
+
if (rotation.retired) await this.ctx.storage.put(RETIRED_SNAPSHOT_KEY, rotation.retired);
|
|
3188
|
+
if (rotation.deleteIds.length > 0) await this.deleteBackupObjects(rotation.deleteIds).catch(() => {});
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
private retiredSnapshot(): Promise<SnapshotRecord | undefined> {
|
|
3192
|
+
return this.ctx.storage.get<SnapshotRecord>(RETIRED_SNAPSHOT_KEY);
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
/** The backup ids of every recorded generation — the current pair and the
|
|
3196
|
+
* retired one — for the paths that delete or itemize them all. */
|
|
3197
|
+
private async recordedBackupIds(current: SnapshotRecord | undefined): Promise<string[]> {
|
|
3198
|
+
return backupIdsOf([current, await this.retiredSnapshot()]);
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3154
3201
|
/** The cycle's completion: the facts to the stamp (the snapshot step moved
|
|
3155
3202
|
* them in the same write as the record — a wake never sees a half-updated
|
|
3156
3203
|
* pair; this is the cycle's own bookkeeping on a fresh read, and a
|
|
@@ -4067,12 +4114,36 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4067
4114
|
committedKiB: this.diskCommittedKiB,
|
|
4068
4115
|
diskBudgetMb: input.record.diskBudgetMb,
|
|
4069
4116
|
kind,
|
|
4117
|
+
// A deps-store entry backup stages its archive from the deps (item
|
|
4118
|
+
// 61); the reserve holds their share only while one runs.
|
|
4119
|
+
depsBackupInFlight: this.depsBackupsInFlight.size > 0,
|
|
4070
4120
|
});
|
|
4071
4121
|
let rawFree = df.freeKiB;
|
|
4072
4122
|
let verdict = decide(rawFree);
|
|
4073
4123
|
const evicted: Array<{ threadKey: string; freedKiB: number | null }> = [];
|
|
4074
4124
|
// Item 62: keep decisions travel as tokens; the key stays for the log line.
|
|
4075
4125
|
const kept: Array<{ threadKey: string; why: DiskKeepWhy }> = [];
|
|
4126
|
+
// The deps store's spares go first (item 55): a warm cache no live tree
|
|
4127
|
+
// references, worth minutes on a future attach, against a tree refused
|
|
4128
|
+
// now. Their bytes come back before any idle tree is considered.
|
|
4129
|
+
let spares: Array<{ key: string; freedKiB: number }> = [];
|
|
4130
|
+
if (!verdict.fits) {
|
|
4131
|
+
spares = await this.evictDepsSparesUnderPressure().catch((err) => {
|
|
4132
|
+
console.log(`disk-pressure: deps spare eviction failed: ${errMsg(err)}`);
|
|
4133
|
+
return [];
|
|
4134
|
+
});
|
|
4135
|
+
if (spares.length > 0) {
|
|
4136
|
+
console.log(
|
|
4137
|
+
`disk-pressure: evicted ${spares.length} deps-store spare(s) (${formatGiB(spares.reduce((a, e) => a + e.freedKiB, 0))} back) to make room for ${input.threadKey}`,
|
|
4138
|
+
);
|
|
4139
|
+
rawFree = rawFreeAfterEviction(
|
|
4140
|
+
rawFree,
|
|
4141
|
+
await this.dfSample(),
|
|
4142
|
+
spares.reduce((a, e) => a + e.freedKiB, 0),
|
|
4143
|
+
);
|
|
4144
|
+
verdict = decide(rawFree);
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4076
4147
|
if (!verdict.fits) {
|
|
4077
4148
|
const live = await this.liveBindings();
|
|
4078
4149
|
const candidates: DiskEvictionCandidate[] = live.map((b) => ({
|
|
@@ -4119,7 +4190,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4119
4190
|
// truthiness does not narrow a boolean-literal discriminant.
|
|
4120
4191
|
const final = verdict;
|
|
4121
4192
|
if (final.fits === false) {
|
|
4122
|
-
const reason = diskPressureReason({ verdict: final, evicted, kept });
|
|
4193
|
+
const reason = diskPressureReason({ verdict: final, evicted, kept, spares });
|
|
4123
4194
|
console.log(`attach ${input.threadKey}: ${reason}`);
|
|
4124
4195
|
const s = await this.getStatus();
|
|
4125
4196
|
return { error: reason, status: 503, state: s.state, reason: DISK_PRESSURE_REASON };
|
|
@@ -4127,7 +4198,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4127
4198
|
const m = final.math;
|
|
4128
4199
|
console.log(
|
|
4129
4200
|
`attach ${input.threadKey}: disk admitted — ${kind} ${formatGiB(m.projectedKiB)} projected, ${formatGiB(m.freeKiB)} free, ` +
|
|
4130
|
-
`reserve ${formatGiB(m.reserve.totalKiB)}, headroom ${formatGiB(m.headroomKiB)}${evicted.length > 0 ? `, evicted ${evicted.map((e) => e.threadKey).join(", ")}` : ""}`,
|
|
4201
|
+
`reserve ${formatGiB(m.reserve.totalKiB)}, headroom ${formatGiB(m.headroomKiB)}${spares.length > 0 ? `, evicted ${spares.length} deps spare(s)` : ""}${evicted.length > 0 ? `, evicted ${evicted.map((e) => e.threadKey).join(", ")}` : ""}`,
|
|
4131
4202
|
);
|
|
4132
4203
|
const committedKiB = m.projectedKiB ?? 0;
|
|
4133
4204
|
this.diskCommittedKiB += committedKiB;
|
|
@@ -4461,12 +4532,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4461
4532
|
lastAttachAt: now,
|
|
4462
4533
|
evicted: false,
|
|
4463
4534
|
// An evicted binding's own history: how its ref was chosen (absent on
|
|
4464
|
-
// one made before the field — never overwritten by this attach's flag)
|
|
4465
|
-
//
|
|
4535
|
+
// one made before the field — never overwritten by this attach's flag),
|
|
4536
|
+
// the one move it may have made and its last move back.
|
|
4466
4537
|
...(existing
|
|
4467
4538
|
? {
|
|
4468
4539
|
...(existing.boundBy !== undefined ? { boundBy: existing.boundBy } : {}),
|
|
4469
4540
|
...(existing.rebound !== undefined ? { rebound: existing.rebound } : {}),
|
|
4541
|
+
...(existing.returned !== undefined ? { returned: existing.returned } : {}),
|
|
4470
4542
|
...(existing.ownBranches !== undefined ? { ownBranches: existing.ownBranches } : {}),
|
|
4471
4543
|
}
|
|
4472
4544
|
: { boundBy }),
|
|
@@ -4819,27 +4891,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4819
4891
|
|
|
4820
4892
|
/** Item 16's second movement, decided where the fact is established — under
|
|
4821
4893
|
* the mirror mutex, after the attach's fetch found the bound ref gone: a
|
|
4822
|
-
* binding a
|
|
4823
|
-
*
|
|
4824
|
-
*
|
|
4825
|
-
*
|
|
4826
|
-
*
|
|
4827
|
-
*
|
|
4828
|
-
*
|
|
4829
|
-
*
|
|
4830
|
-
*
|
|
4894
|
+
* binding on a branch that is now deleted (its pull request merged) goes
|
|
4895
|
+
* back to the default, so the run starts clean there (item 17) instead of
|
|
4896
|
+
* the thread failing `unknown-ref` for the rest of its life — when the
|
|
4897
|
+
* branch is one a rebind moved it onto, or one this thread's own runs
|
|
4898
|
+
* pushed as the binding remembers it (`ownBranches`): a ship unit's coding
|
|
4899
|
+
* child binds its unit branch by name and pushes it, and the merge deletes
|
|
4900
|
+
* it. The row as it stands is re-read and re-judged (the pure
|
|
4901
|
+
* `returnToDefault`, which writes nothing for a row `canReturnToDefault`
|
|
4902
|
+
* does not admit: a ref a person named that the thread never pushed never
|
|
4903
|
+
* returns — that branch is theirs to sort out), the move recorded so the
|
|
4904
|
+
* thread may follow its next pull request, and the answer carries the move
|
|
4905
|
+
* back for the card. A row that no longer qualifies — another attach moved
|
|
4906
|
+
* it meanwhile — is answered as it stands, nothing written. */
|
|
4831
4907
|
private async returnBindingToDefault(
|
|
4832
4908
|
binding: ThreadBinding,
|
|
4833
4909
|
defaultRef: string,
|
|
4834
4910
|
): Promise<{ binding: ThreadBinding; returned?: Returned }> {
|
|
4835
4911
|
const key = threadBindingKey(binding.threadKey);
|
|
4836
4912
|
const current = (await this.ctx.storage.get<ThreadBinding>(key)) ?? binding;
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
{ ...current, rebound: current.rebound },
|
|
4840
|
-
defaultRef,
|
|
4841
|
-
new Date(systemClock()).toISOString(),
|
|
4842
|
-
);
|
|
4913
|
+
const back = returnToDefault(current, defaultRef, new Date(systemClock()).toISOString());
|
|
4914
|
+
if (back === undefined) return { binding: current };
|
|
4843
4915
|
await this.ctx.storage.put(key, back.binding);
|
|
4844
4916
|
console.log(
|
|
4845
4917
|
`attach ${binding.threadKey}: ${back.returned.from} is gone from the mirror (the thread's own pull request #${back.returned.pr}) — returned to ${back.returned.to}; the tree is provisioned there`,
|
|
@@ -4921,9 +4993,22 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4921
4993
|
// The expected head applies to the ref it was resolved for; a sticky
|
|
4922
4994
|
// binding on another branch drops it (item 51) rather than fetching on
|
|
4923
4995
|
// every attach of a thread that can never be at that commit.
|
|
4996
|
+
// A ref the binding could return from — a branch a rebind moved it onto,
|
|
4997
|
+
// or one its own runs pushed (item 16's second movement) — is fetched for
|
|
4998
|
+
// whether or not the mirror holds it: the branch dies when its pull
|
|
4999
|
+
// request merges, the deletion reaches the mirror only through a
|
|
5000
|
+
// `fetch --prune`, and a mirror trusted for still holding the ref would
|
|
5001
|
+
// provision a tree at the deleted branch's stale tip until the refresh
|
|
5002
|
+
// cycle's prune caught up. One predicate, read once here, drives the mint
|
|
5003
|
+
// pre-check, the fetch under the lock and the return gate alike.
|
|
4924
5004
|
let want = wantShaForBinding({ boundRef: binding.ref, refHint, wantSha });
|
|
5005
|
+
const returnable = canReturnToDefault(binding, facts.defaultRef);
|
|
4925
5006
|
let fetchToken: string | null = token;
|
|
4926
|
-
if (
|
|
5007
|
+
if (
|
|
5008
|
+
!fetchToken &&
|
|
5009
|
+
githubAppConfigured(this.env) &&
|
|
5010
|
+
(await this.mirrorFetchReasonFor(binding.ref, want, returnable)) !== null
|
|
5011
|
+
) {
|
|
4927
5012
|
fetchToken = (await mintRepoScopedToken(this.env, slug).catch(() => null))?.token ?? null;
|
|
4928
5013
|
}
|
|
4929
5014
|
|
|
@@ -4931,14 +5016,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4931
5016
|
try {
|
|
4932
5017
|
locked = await this.withMirrorLock(async () => {
|
|
4933
5018
|
await this.ensureGitSetup();
|
|
4934
|
-
const
|
|
4935
|
-
if (
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
5019
|
+
const why = await this.mirrorFetchReasonFor(binding.ref, want, returnable);
|
|
5020
|
+
if (why !== null) {
|
|
5021
|
+
try {
|
|
5022
|
+
await this.gitWithCred(
|
|
5023
|
+
fetchToken,
|
|
5024
|
+
["-C", MIRROR_DIR, "fetch", "--prune", "origin"],
|
|
5025
|
+
"fetch",
|
|
5026
|
+
GIT_NETWORK_TIMEOUT_MS,
|
|
5027
|
+
);
|
|
5028
|
+
} catch (err) {
|
|
5029
|
+
// A fetch for a missing or stale ref fails the attach as it always
|
|
5030
|
+
// did. One that only verifies a returnable ref the mirror holds is
|
|
5031
|
+
// not the attach's failure — the origin unreachable, no token for
|
|
5032
|
+
// a private repo — so the attach goes on with the mirror's ref, as
|
|
5033
|
+
// it did before the verification existed; the return is only
|
|
5034
|
+
// ever decided on a fetch that ran.
|
|
5035
|
+
if (why !== "returnable-ref") throw err;
|
|
5036
|
+
console.log(
|
|
5037
|
+
`attach ${threadKey}: the fetch verifying ${binding.ref} at the origin failed (${errMsg(err)}) — the attach goes on with the mirror's ref`,
|
|
5038
|
+
);
|
|
5039
|
+
}
|
|
4942
5040
|
}
|
|
4943
5041
|
// What to check out, now that the mirror is as fresh as it will get
|
|
4944
5042
|
// (item 51, the pure `attachTarget`): the ref's tip when the ref is
|
|
@@ -4947,18 +5045,21 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4947
5045
|
// `refs/pull/N/head` still names its head, and the caller told us that
|
|
4948
5046
|
// head — so a review of a merged PR stays on the warm resident instead
|
|
4949
5047
|
// of falling back to a cold sandbox that clones the same commit itself.
|
|
4950
|
-
// No fetch
|
|
4951
|
-
// for every missing ref, so only a fetched mirror needs the
|
|
4952
|
-
//
|
|
4953
|
-
|
|
5048
|
+
// No fetch was due ⇒ the ref existed at the check: `mirrorFetchReason`
|
|
5049
|
+
// fetches for every missing ref, so only a fetched mirror needs the
|
|
5050
|
+
// re-read — and a returnable ref is re-read for real, the fact the
|
|
5051
|
+
// second movement is decided on. (Under the mirror mutex — the mirror
|
|
5052
|
+
// cannot change in between.)
|
|
5053
|
+
const refExists = why === null || (await this.refExists(binding.ref));
|
|
4954
5054
|
let target = attachTarget({
|
|
4955
5055
|
refExists,
|
|
4956
5056
|
wantSha: want,
|
|
4957
5057
|
commitInMirror: !refExists && want !== null && (await this.commitInMirror(want)),
|
|
4958
5058
|
});
|
|
4959
|
-
if (target.kind === "unknown-ref" &&
|
|
4960
|
-
// The branch a rebind moved
|
|
4961
|
-
//
|
|
5059
|
+
if (target.kind === "unknown-ref" && returnable) {
|
|
5060
|
+
// The thread's branch — one a rebind moved it onto, or one its own
|
|
5061
|
+
// runs pushed and it was bound to by name — is gone even after the
|
|
5062
|
+
// fetch: deleted once its pull request merged. The binding goes
|
|
4962
5063
|
// back to the default (item 16's second movement) and the attach
|
|
4963
5064
|
// goes on there: the default is always in the mirror, and the
|
|
4964
5065
|
// worktree step below provisions the tree at its tip.
|
|
@@ -5713,27 +5814,24 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5713
5814
|
* in flight, and remove what `planDepsEviction` names — debris first, then
|
|
5714
5815
|
* the coldest spares beyond DEPS_STORE_MAX_UNREFERENCED. Housekeeping:
|
|
5715
5816
|
* a failure is a log line, never a lifecycle flip. */
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
const listing = parseDepsStoreListing(listed.stdout);
|
|
5720
|
-
if (listing.entries.length === 0 && listing.leftovers.length === 0) return;
|
|
5817
|
+
/** The store entries no eviction may touch: the checkout's key, every live
|
|
5818
|
+
* binding's, every install and every entry backup in flight. */
|
|
5819
|
+
private async depsProtectedKeys(): Promise<Set<string>> {
|
|
5721
5820
|
const facts = await this.ctx.storage.get<RepoFacts>(FACTS_KEY);
|
|
5722
5821
|
const protectedKeys = new Set<string>([...this.depsInFlight.keys(), ...this.depsBackupsInFlight]);
|
|
5723
5822
|
if (facts?.lockfileHash) protectedKeys.add(facts.lockfileHash);
|
|
5724
5823
|
for (const b of await this.liveBindings()) if (b.depsKey) protectedKeys.add(b.depsKey);
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5824
|
+
return protectedKeys;
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5827
|
+
/** Remove the store entries a plan names and drop their entry backups with
|
|
5828
|
+
* them (item 61 PR B: a spare nothing references on disk is a spare nothing
|
|
5829
|
+
* will wake into). Shared by the sweep and the attach's pressure path. */
|
|
5830
|
+
private async applyDepsEviction(plan: DepsEvictionPlan, listing: DepsStoreListing, who: string): Promise<void> {
|
|
5731
5831
|
await this.runOk(["rm", "-rf", ...plan.remove], "deps-evict", { timeoutMs: DU_TIMEOUT_MS });
|
|
5732
5832
|
console.log(
|
|
5733
|
-
`deps: evicted ${plan.remove.length} path(s) — ${plan.remove.map((p) => p.slice(DEPS_STORE_DIR.length + 1, DEPS_STORE_DIR.length + 9)).join(", ")}; kept ${plan.keep.map((k) => k.slice(0, 8)).join(", ")}`,
|
|
5833
|
+
`deps: ${who} evicted ${plan.remove.length} path(s) — ${plan.remove.map((p) => p.slice(DEPS_STORE_DIR.length + 1, DEPS_STORE_DIR.length + 9)).join(", ")}; kept ${plan.keep.map((k) => k.slice(0, 8)).join(", ")}`,
|
|
5734
5834
|
);
|
|
5735
|
-
// The evicted entries' archives go with them (item 61 PR B): a spare
|
|
5736
|
-
// nothing references on disk is a spare nothing will wake into.
|
|
5737
5835
|
const kept = new Set(plan.keep);
|
|
5738
5836
|
const evictedKeys = listing.entries.map((e) => e.key).filter((k) => !kept.has(k));
|
|
5739
5837
|
const drop = depsBackupsToDrop({
|
|
@@ -5748,6 +5846,44 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5748
5846
|
}
|
|
5749
5847
|
}
|
|
5750
5848
|
|
|
5849
|
+
/** Under disk pressure (item 55) the store keeps no spare: every complete
|
|
5850
|
+
* entry no live tree references goes, coldest first, before any idle tree
|
|
5851
|
+
* is considered — a spare is a cache worth minutes on a future attach, a
|
|
5852
|
+
* refused tree is a run falling to a cold sandbox now. Leftovers are the
|
|
5853
|
+
* sweep's business (they may be live installs' scratch). Answers what it
|
|
5854
|
+
* removed with the bytes each measured at; a listing that fails answers
|
|
5855
|
+
* nothing and the tree path decides as before. */
|
|
5856
|
+
private async evictDepsSparesUnderPressure(): Promise<Array<{ key: string; freedKiB: number }>> {
|
|
5857
|
+
const listed = await this.run(["sh", "-c", depsStoreListScript()], { timeoutMs: DU_TIMEOUT_MS });
|
|
5858
|
+
if (listed.exitCode !== 0) return [];
|
|
5859
|
+
const listing = parseDepsStoreListing(listed.stdout);
|
|
5860
|
+
if (listing.entries.length === 0) return [];
|
|
5861
|
+
const plan = planDepsEviction({
|
|
5862
|
+
entries: listing.entries,
|
|
5863
|
+
leftovers: [],
|
|
5864
|
+
protectedKeys: await this.depsProtectedKeys(),
|
|
5865
|
+
maxUnreferenced: DEPS_STORE_MAX_UNREFERENCED_UNDER_PRESSURE,
|
|
5866
|
+
});
|
|
5867
|
+
if (plan.remove.length === 0) return [];
|
|
5868
|
+
await this.applyDepsEviction(plan, listing, "disk-pressure");
|
|
5869
|
+
return plan.evicted.map((e) => ({ key: e.key, freedKiB: e.kib }));
|
|
5870
|
+
}
|
|
5871
|
+
|
|
5872
|
+
private async sweepDepsStore(): Promise<void> {
|
|
5873
|
+
const listed = await this.run(["sh", "-c", depsStoreListScript()], { timeoutMs: DU_TIMEOUT_MS });
|
|
5874
|
+
if (listed.exitCode !== 0) return;
|
|
5875
|
+
const listing = parseDepsStoreListing(listed.stdout);
|
|
5876
|
+
if (listing.entries.length === 0 && listing.leftovers.length === 0) return;
|
|
5877
|
+
const protectedKeys = await this.depsProtectedKeys();
|
|
5878
|
+
// A scratch/staging dir of an install still running in this incarnation
|
|
5879
|
+
// is live work, not debris; the listing cannot tell them apart, so
|
|
5880
|
+
// leftovers wait for a sweep with nothing in flight.
|
|
5881
|
+
const leftovers = this.depsInFlight.size > 0 ? [] : listing.leftovers;
|
|
5882
|
+
const plan = planDepsEviction({ entries: listing.entries, leftovers, protectedKeys });
|
|
5883
|
+
if (plan.remove.length === 0) return;
|
|
5884
|
+
await this.applyDepsEviction(plan, listing, "sweep");
|
|
5885
|
+
}
|
|
5886
|
+
|
|
5751
5887
|
/** The per-user 700 staging dir (`install -d` is idempotent), memoized per
|
|
5752
5888
|
* incarnation — the write/credential paths used to fork for it on every
|
|
5753
5889
|
* call. Cleared with the other memos; a fresh disk simply re-creates it. */
|
|
@@ -7049,6 +7185,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7049
7185
|
};
|
|
7050
7186
|
}
|
|
7051
7187
|
const snap = await this.ctx.storage.get<SnapshotRecord>(SNAPSHOT_KEY);
|
|
7188
|
+
const recorded = await this.recordedBackupIds(snap);
|
|
7052
7189
|
const bindings = await this.ctx.storage.list<ThreadBinding>({ prefix: THREAD_KEY_PREFIX });
|
|
7053
7190
|
const plan = {
|
|
7054
7191
|
resource,
|
|
@@ -7065,7 +7202,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7065
7202
|
checkoutBackupId: snap.checkout.id,
|
|
7066
7203
|
}
|
|
7067
7204
|
: null,
|
|
7068
|
-
backupObjects:
|
|
7205
|
+
backupObjects: await this.countBackupObjects(recorded),
|
|
7069
7206
|
},
|
|
7070
7207
|
reprovision: { defaultRef, provisioningTimeoutMs },
|
|
7071
7208
|
keeps: { registryRecord: true as const, threadBindings: bindings.size },
|
|
@@ -7076,9 +7213,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7076
7213
|
// and backups/<id>/ lives outside the resident/<resource>/ prefix — this
|
|
7077
7214
|
// is the only path that can still reach them (same ordering as teardown).
|
|
7078
7215
|
let backupObjectsDeleted = 0;
|
|
7079
|
-
if (
|
|
7216
|
+
if (recorded.length > 0) {
|
|
7080
7217
|
try {
|
|
7081
|
-
backupObjectsDeleted = await this.deleteBackupObjects(
|
|
7218
|
+
backupObjectsDeleted = await this.deleteBackupObjects(recorded);
|
|
7082
7219
|
} catch {
|
|
7083
7220
|
// best effort — the 1-year R2 TTL is the leak backstop
|
|
7084
7221
|
}
|
|
@@ -7114,8 +7251,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7114
7251
|
this.listSchedules(PROVISIONING_CALLBACK),
|
|
7115
7252
|
this.listSchedules(PROVISION_RUN_CALLBACK),
|
|
7116
7253
|
]);
|
|
7117
|
-
const
|
|
7118
|
-
const ids = snap ? [snap.mirror.id, snap.checkout.id] : [];
|
|
7254
|
+
const ids = await this.recordedBackupIds(await this.ctx.storage.get<SnapshotRecord>(SNAPSHOT_KEY));
|
|
7119
7255
|
const bindings = await this.ctx.storage.list<ThreadBinding>({ prefix: THREAD_KEY_PREFIX });
|
|
7120
7256
|
return {
|
|
7121
7257
|
...status,
|
|
@@ -7143,10 +7279,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7143
7279
|
let backupObjectsDeleted = 0;
|
|
7144
7280
|
this.deleteSchedules(PROVISIONING_CALLBACK);
|
|
7145
7281
|
this.deleteSchedules(PROVISION_RUN_CALLBACK);
|
|
7146
|
-
const
|
|
7147
|
-
if (
|
|
7282
|
+
const recorded = await this.recordedBackupIds(await this.ctx.storage.get<SnapshotRecord>(SNAPSHOT_KEY));
|
|
7283
|
+
if (recorded.length > 0) {
|
|
7148
7284
|
try {
|
|
7149
|
-
backupObjectsDeleted = await this.deleteBackupObjects(
|
|
7285
|
+
backupObjectsDeleted = await this.deleteBackupObjects(recorded);
|
|
7150
7286
|
} catch (err) {
|
|
7151
7287
|
errors.push(`backup object deletion failed: ${errMsg(err)}`);
|
|
7152
7288
|
}
|
|
@@ -41,6 +41,9 @@ import {
|
|
|
41
41
|
SANDBOX_SLEEP_AFTER,
|
|
42
42
|
isRecycleError,
|
|
43
43
|
recycledMidCommandMessage,
|
|
44
|
+
DETACH_HINT,
|
|
45
|
+
OUTPUT_AFTER_EXIT_MS,
|
|
46
|
+
heldOutputNote,
|
|
44
47
|
} from "../../src/execution/sandboxLifecycle.js";
|
|
45
48
|
import { envFromRequest } from "../../src/execution/sandboxEnv.js";
|
|
46
49
|
import { IdleGuard, type IdleGuardHost } from "../../src/execution/sandboxIdle.js";
|
|
@@ -88,10 +91,17 @@ const EXEC_TIMEOUT_SECS = 280;
|
|
|
88
91
|
* the shell-level 124 is always the deadline a command meets first. */
|
|
89
92
|
const SDK_BACKSTOP_MARGIN_MS = 40_000;
|
|
90
93
|
|
|
91
|
-
/** How long the Durable Object waits for the process's
|
|
92
|
-
* own
|
|
93
|
-
*
|
|
94
|
-
|
|
94
|
+
/** How long the Durable Object waits for the process's output to END past
|
|
95
|
+
* the command's own deadline before it asks the runtime what became of the
|
|
96
|
+
* process (docs/reference/specs/execution.md item 24). The runtime ends the
|
|
97
|
+
* output stream only when every holder of the command's stdout and stderr is
|
|
98
|
+
* gone; a process that exited while a detached child holds them is answered
|
|
99
|
+
* with its exit code and a note, a process still running is killed and
|
|
100
|
+
* answered as the shell-level timeout. The wait is `OUTPUT_AFTER_EXIT_MS`
|
|
101
|
+
* (20 s), inside the executor's per-send margin, so this answer always
|
|
102
|
+
* reaches the bot before it gives the command up — the SDK's own limit
|
|
103
|
+
* (`SDK_BACKSTOP_MARGIN_MS`) stays above it as the last resort. */
|
|
104
|
+
const OUTPUT_WAIT_AFTER_DEADLINE_MS = OUTPUT_AFTER_EXIT_MS;
|
|
95
105
|
|
|
96
106
|
/** The interruption reasons that mean the container's runtime is not the one
|
|
97
107
|
* the command started on: the process, if it started, is gone with its
|
|
@@ -303,7 +313,10 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
303
313
|
return this.execFailure(err, startedAt);
|
|
304
314
|
}
|
|
305
315
|
try {
|
|
306
|
-
const out = await proc.output({
|
|
316
|
+
const out = await proc.output({
|
|
317
|
+
encoding: "utf8",
|
|
318
|
+
timeout: execTimeoutSecs * 1000 + OUTPUT_WAIT_AFTER_DEADLINE_MS,
|
|
319
|
+
});
|
|
307
320
|
// coreutils `timeout` exits 124 when the deadline killed the command
|
|
308
321
|
// (137 when the follow-up SIGKILL had to); the SDK's own limit, if it
|
|
309
322
|
// ever wins, reports `timedOut`. All three are the one story.
|
|
@@ -321,9 +334,21 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
321
334
|
};
|
|
322
335
|
} catch (err) {
|
|
323
336
|
if (err instanceof ProcessWaitTimeoutError) {
|
|
324
|
-
// The
|
|
325
|
-
//
|
|
326
|
-
//
|
|
337
|
+
// The output never ended inside the command's deadline plus the
|
|
338
|
+
// margin. Two stories, told apart by asking the runtime (item 24):
|
|
339
|
+
// the process EXITED and a detached child holds its output open —
|
|
340
|
+
// the answer is its exit code and the note, the child runs on; or
|
|
341
|
+
// the process is still RUNNING past its own `timeout` — end it, and
|
|
342
|
+
// report the shell-level timeout it exceeded.
|
|
343
|
+
const status = await proc.status().catch(() => null);
|
|
344
|
+
if (status?.state === "exited") {
|
|
345
|
+
return {
|
|
346
|
+
stdout: "",
|
|
347
|
+
stderr: heldOutputNote(status.exit.code),
|
|
348
|
+
exitCode: status.exit.code,
|
|
349
|
+
durationMs: systemClock() - startedAt,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
327
352
|
await proc.kill(9).catch(() => {});
|
|
328
353
|
return {
|
|
329
354
|
stdout: "",
|
|
@@ -449,11 +474,12 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
449
474
|
/** The stderr line an exit 124 carries: the limit, the knob, and the way to
|
|
450
475
|
* outlive a command (`setsid -f`: every /exec runs under `timeout … bash -c`,
|
|
451
476
|
* whose process group is reaped when the command returns, so a plain
|
|
452
|
-
* background job dies with it
|
|
477
|
+
* background job dies with it — and with the wrapper's stdio redirected, so
|
|
478
|
+
* the command's output ends when the command does, item 24). */
|
|
453
479
|
function timeoutNote(execTimeoutSecs: number): string {
|
|
454
480
|
return (
|
|
455
481
|
`command timed out in the sandbox after ${execTimeoutSecs}s (pass the bash tool's timeoutMs for longer commands, max ${BASH_TIMEOUT_MAX_MS} ms); ` +
|
|
456
|
-
|
|
482
|
+
`re-run as smaller/faster steps, or start it detached with \`${DETACH_HINT}\` and poll the log on later calls`
|
|
457
483
|
);
|
|
458
484
|
}
|
|
459
485
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.238.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.238.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -20241,7 +20241,7 @@
|
|
|
20241
20241
|
},
|
|
20242
20242
|
"packages/switchboard": {
|
|
20243
20243
|
"name": "@coreplane/switchboard",
|
|
20244
|
-
"version": "1.
|
|
20244
|
+
"version": "1.238.0",
|
|
20245
20245
|
"license": "Apache-2.0",
|
|
20246
20246
|
"dependencies": {
|
|
20247
20247
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.238.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/source.json
CHANGED