@coreplane/switchboard 1.235.0 → 1.237.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/Dockerfile +11 -0
- package/dist/assets/config/config.example.yaml +9 -4
- package/dist/assets/deploy/cloudflare-memory/worker.ts +12 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +18 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +290 -118
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +104 -17
- package/dist/assets/package-lock.json +212 -3
- package/dist/assets/package.json +4 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/authz/authorize.ts +25 -6
- package/dist/assets/src/core/authz/policy.ts +9 -0
- package/dist/assets/src/core/authz/types.ts +15 -1
- package/dist/assets/src/core/coordinator/contract.ts +24 -4
- package/dist/assets/src/core/coordinator/driver.ts +36 -4
- package/dist/assets/src/core/runEvents.ts +46 -7
- package/dist/assets/src/core/runFriction.ts +1 -0
- package/dist/assets/src/core/ship/contract.ts +3 -1
- package/dist/assets/src/core/ship/coordinator.ts +236 -30
- package/dist/assets/src/core/trace/types.ts +5 -0
- package/dist/assets/src/execution/residentDepsStore.ts +14 -2
- package/dist/assets/src/execution/residentDisk.ts +21 -10
- package/dist/assets/src/execution/residentDiskBudget.ts +45 -11
- package/dist/assets/src/execution/residentHead.ts +34 -8
- package/dist/assets/src/execution/residentRebind.ts +88 -31
- package/dist/assets/src/execution/sandboxErrors.ts +72 -0
- package/dist/assets/src/execution/sandboxLifecycle.ts +33 -0
- package/dist/assets/src/execution/sandboxStart.ts +90 -0
- package/dist/assets/web/dist/.vite/manifest.json +437 -423
- package/dist/assets/web/dist/assets/AppShell-MYHNxJks.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-D-v8am88.js → CostsPage-DKk9OjB0.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-CvlWP7Eq.js → DeliveryPage-Kz6js49i.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-BzWS4Ca1.js → NotFoundPage-BXLrQQzy.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-Dc23v_8v.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-TR5D1TRR.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-mkUj7WEN.js +9 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-oN9GkVEA.js → RunRoutePage-CQI17__g.js} +3 -3
- package/dist/assets/web/dist/assets/RunsIndexPage-yIP4PlKP.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-DEICQ4FY.js → RunsTabs-XpCqeWu4.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-CdCVyNbh.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DuA8JpLU.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-D14iJMy7.js → StatusDot-DmNHX6am.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-B0Ob5MQ4.js → Tooltip-qVnJTgSt.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-BcTeH7m2.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Cw130Zyi.js → angular-html-DYtV9uIN.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DY1m4C2s.js → angular-ts-DF2P25Zp.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-W6Ty7v05.js → apl-Ff96wxko.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DlLVHwVk.js → astro-LbBnZna-.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-Bco3tDh4.js → blade-DltuxeBW.js} +1 -1
- package/dist/assets/web/dist/assets/{c-1LaFY_cj.js → c-DBg7yuPx.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-B-OvrOL5.js → chapel-C83hUAds.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-Ch3EPlHu.js → cobol-DB0yV_2D.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-hZuITnto.js → coffee-Bpih6Kzl.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-B8iRVo8m.js → cpp-CU00ZeRn.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DKveI7lr.js → crystal-f8ImJkux.js} +1 -1
- package/dist/assets/web/dist/assets/{css-_2_CD-rr.js → css-4VrC4ISo.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-pOdUzj6D.js → dist-TAGawIfM.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-CYMux25J.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BenMDPct.js → edge-BwFDzfS_.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-CBbdEqG6.js → elixir-wwIYIpJm.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DMub_2lY.js → elm-DyDuM3v3.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CI9He_Up.js → erb-DFJBEVrh.js} +1 -1
- package/dist/assets/web/dist/assets/{favicon-BQsePYv5.js → favicon-CSt-qDvc.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-BBfhnyF8.js → git-rebase-CuDv-44X.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-CR9Fxrau.js → glimmer-js-ZFnrm4J3.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CwFwX_XS.js → glimmer-ts-XY4Yqshp.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Daiodp1r.js → glsl-xnR8efx0.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-DZvrsEkB.js → graphql-D-gO5tnP.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BaXOzdg7.js → hack-CSxbW4ax.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CMskq_VA.js → haml-D4ar42QT.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-Dhw_f7jD.js → handlebars-CMLbDE7o.js} +1 -1
- package/dist/assets/web/dist/assets/{html-C9k99z0z.js → html-CVekU4Fs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-VckPItXN.js → html-derivative-St3HHw_N.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXU_3h9v.js → http-Mon0DWHS.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-KU24JNdp.js → hurl-Cbl5IvBs.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-FLJu5mR9.js +1 -0
- package/dist/assets/web/dist/assets/{java-BbZjTNgf.js → java-Bprck1l-.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-DXCdqcZZ.js → javascript-C73G1-UR.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-pDlsjTVp.js → jinja-C4DuRxYJ.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DsXDk66e.js → jison-B2U03GEC.js} +1 -1
- package/dist/assets/web/dist/assets/{json-tCxXfRgG.js → json-CvTZUDzx.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-B0ZYkCmi.js → jsx-DMWKCzwo.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-BLzzeI6x.js → julia-Czn7bV_3.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DvD2_60c.js → just-BBxpNfvC.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-511zn3h7.js → latex-Bw9yg5mr.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BD0BkMox.js → liquid-BRCjaydE.js} +1 -1
- package/dist/assets/web/dist/assets/{indexFormat-B-pd4r7Y.js → localIso-CNA-bhS-.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-NFicKm6N.js → lua-DUu2KIC0.js} +1 -1
- package/dist/assets/web/dist/assets/main-csihqEj5.css +1 -0
- package/dist/assets/web/dist/assets/main-mUd_TJjH.js +28 -0
- package/dist/assets/web/dist/assets/{marko-1-G_nEXK.js → marko-CZJZIlSQ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DUe3AQLp.js → mdc-BmClq6fd.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Dg379_bA.js → nginx-Bmt3xSEm.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BnFq97ZO.js → nim-Da-i0utK.js} +1 -1
- package/dist/assets/web/dist/assets/{org-F0uiwGvq.js → org-BvAre2qP.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-B0euczkl.js → perl-v9WwFFtB.js} +1 -1
- package/dist/assets/web/dist/assets/{php-DkL3k_n7.js → php-DTU4Zyze.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-L_OjjZP4.js → pug-DhTwEqTO.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BMjB00Zz.js → qml-BUD9AC_c.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DoeLdnqR.js → r-CAnu56n7.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BzYNkAWy.js → razor-Dn0P4gAq.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CPmElMk3.js → regexp-CZyBRaVW.js} +1 -1
- package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +1 -0
- package/dist/assets/web/dist/assets/{rst-DrsjgfI7.js → rst-Ckyd5UsY.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cB42ppvx.js → ruby-BBQ62fsx.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-R5Y3NM_I.js → sas-CW35hMxG.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-C-zWxV9s.js → scss-CpV8pF4J.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-oJF96aAU.js → shellscript-CRJvsmE1.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CznECKyi.js → shellsession-CyriUZxf.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-wjHLSRag.js → soy-D5QzYUeg.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-C_BM8IOW.js → sql-CUBf_Mse.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-D31HO8aQ.js → stata-B-4n4ITz.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CKCLyplA.js → surrealql-RuWZBLtr.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-3geWk2iu.js → svelte-CO5YzCsK.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DiJr_hTD.js → templ-DbkWVaBF.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BEJGWyeF.js → tex-IuxoEcId.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-C8Mzdpxx.js → ts-tags-g6dGQPjd.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-UQiSm_p3.js → tsx-WnXwiyH2.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-_Pvzkeo9.js → twig-Lx1G7PnY.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BWMkINKK.js → typescript-BQd7biLF.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Bdg9m-e7.js → typst-D2lXPHK7.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CDZrrAZ8.js → vue-4wVQD9y9.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-tAlpNhfN.js → vue-html-DZPaRte4.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-C27UF_rh.js → vue-vine-DbmZCmu-.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-CxeDr9Zh.js → xml-DV16szyo.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bpi0uUnM.js → xsl-eo5-1sk0.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BtUgZ1GN.js → yaml-BXdX8HZf.js} +1 -1
- package/dist/cli.js +3329 -1804
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-lYVcj-k7.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DSpsye5e.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CT8fI1M3.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-Cqh8pTXC.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-sjXr8iCV.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-Tz__zLzi.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CbKCL58v.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-Bnj883ii.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-D3lG-yQ9.js +0 -28
- package/dist/assets/web/dist/assets/main-Dm11o0hc.css +0 -1
- package/dist/assets/web/dist/assets/residentDiskBudget-BMBKlYRH.js +0 -1
|
@@ -252,7 +252,13 @@ import {
|
|
|
252
252
|
type DiskSample,
|
|
253
253
|
type ThreadCostKind,
|
|
254
254
|
} from "../../src/execution/residentDiskBudget.js";
|
|
255
|
-
import {
|
|
255
|
+
import {
|
|
256
|
+
attachTarget,
|
|
257
|
+
type FetchReason,
|
|
258
|
+
mirrorFetchReason,
|
|
259
|
+
parseWantSha,
|
|
260
|
+
wantShaForBinding,
|
|
261
|
+
} from "../../src/execution/residentHead.js";
|
|
256
262
|
import { residentText, sanitizeResidentBody } from "../../src/execution/residentText.js";
|
|
257
263
|
import {
|
|
258
264
|
abandonedWaitStepResult,
|
|
@@ -297,6 +303,9 @@ import {
|
|
|
297
303
|
parseDepsStoreListing,
|
|
298
304
|
planDepsEviction,
|
|
299
305
|
planDepsMaterialization,
|
|
306
|
+
DEPS_STORE_MAX_UNREFERENCED_UNDER_PRESSURE,
|
|
307
|
+
type DepsEvictionPlan,
|
|
308
|
+
type DepsStoreListing,
|
|
300
309
|
} from "../../src/execution/residentDepsStore.js";
|
|
301
310
|
import { buildId, injectedBuildStamp } from "../../src/deploy/buildStamp.js";
|
|
302
311
|
import { createRefreshInstance, createRefreshInstanceNow, type RefreshInstanceParams } from "./refresh";
|
|
@@ -505,16 +514,18 @@ const FORCE_DETACH_KILL_TIMEOUT_MS = 2_000;
|
|
|
505
514
|
* binding record is KEPT so the next attach recreates with the same
|
|
506
515
|
* ref. Overridable per resident via the onboard-time `worktreeTtlDays`. */
|
|
507
516
|
const WORKTREE_TTL_DAYS_DEFAULT = 7;
|
|
508
|
-
/** A live binding whose last attach is older than this
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* release call was lost.
|
|
517
|
+
/** A live binding whose last attach is older than this and has no op in
|
|
518
|
+
* flight is released by the sweep step (every refresh instance runs one),
|
|
519
|
+
* whatever its tree holds (item 17) — runs that ended before /detach existed,
|
|
520
|
+
* or whose release call was lost. */
|
|
512
521
|
const CLEAN_IDLE_RELEASE_S = 60 * 60;
|
|
513
|
-
/** Idle sleep: when no
|
|
514
|
-
*
|
|
515
|
-
* next instance to the idle cadence, so the container can
|
|
516
|
-
* (SLEEP_AFTER); the next attach refreshes first if the mirror
|
|
517
|
-
* (refresh-on-attach).
|
|
522
|
+
/** Idle sleep: when no live binding was attached to or used within this
|
|
523
|
+
* window and nothing is in flight, the refresh cycle skips the fetch and the
|
|
524
|
+
* cron holds the next instance to the idle cadence, so the container can
|
|
525
|
+
* actually sleep (SLEEP_AFTER); the next attach refreshes first if the mirror
|
|
526
|
+
* is stale (refresh-on-attach). The disk-full recycle (item 54) reads the
|
|
527
|
+
* same window through the same predicate (`recentlyUsed`): a sleep and a
|
|
528
|
+
* recycle destroy the same disk, so one rule says when it may go away. */
|
|
518
529
|
const IDLE_AFTER_S = 60 * 60;
|
|
519
530
|
/** LRU eviction floor: an over-cap onboard with `evictColdest:true` may
|
|
520
531
|
* offboard the coldest eligible warm resident, but never one whose last
|
|
@@ -1000,10 +1011,11 @@ interface ResidentRecord {
|
|
|
1000
1011
|
interface ThreadBinding {
|
|
1001
1012
|
threadKey: string;
|
|
1002
1013
|
ref: string;
|
|
1003
|
-
/** How `ref` was chosen
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1014
|
+
/** How `ref` was chosen: the repo default for want of a named branch — when
|
|
1015
|
+
* the binding was made (item 30), or when a return put it there because the
|
|
1016
|
+
* branch it was on is gone (`returned`) — or a branch someone named. Absent
|
|
1017
|
+
* on a binding made before the field: read as `default` iff `ref` is the
|
|
1018
|
+
* default branch. Only a `default` binding may ever move onto a branch. */
|
|
1007
1019
|
boundBy?: BoundBy;
|
|
1008
1020
|
/** The move a binding may make (item 16): from the default it was bound to,
|
|
1009
1021
|
* onto the branch the thread's own run opened a pull request on. Set, the
|
|
@@ -1011,6 +1023,12 @@ interface ThreadBinding {
|
|
|
1011
1023
|
* branch was gone from the mirror and the binding went back to the
|
|
1012
1024
|
* default), and then for the next own pull request. */
|
|
1013
1025
|
rebound?: Rebound;
|
|
1026
|
+
/** The binding's last move back to the default (item 16's second movement):
|
|
1027
|
+
* the branch it was on — one a rebind moved it onto, or one this thread's
|
|
1028
|
+
* own runs pushed (`ownBranches`) and bound by name, a ship unit's — was
|
|
1029
|
+
* gone from the mirror at an attach. From where, to where, which pull
|
|
1030
|
+
* request's branch it was, when. */
|
|
1031
|
+
returned?: Returned;
|
|
1014
1032
|
/** The branches the thread's own runs pushed and the pull requests they
|
|
1015
1033
|
* head, as each run's release handed them over (`/detach` `pushed`, item
|
|
1016
1034
|
* 16a) — written before any eviction decision, so the fact outlives the
|
|
@@ -1037,6 +1055,15 @@ interface ThreadBinding {
|
|
|
1037
1055
|
/** Why the tree could not be measured before that eviction (git could not
|
|
1038
1056
|
* read it) — so an unreadable tree is never recorded as clean. */
|
|
1039
1057
|
evictedUnmeasured?: string;
|
|
1058
|
+
/** What the tree held when the last disk-full recycle (item 54) discarded
|
|
1059
|
+
* it with the disk — the same measurement as `evictedLeftBehind`, under its
|
|
1060
|
+
* own name because the binding is not evicted: its user is kept and the
|
|
1061
|
+
* next attach recreates the tree (`worktree-missing`). Both rewritten on
|
|
1062
|
+
* every recycle; absent when the tree was clean or already gone. */
|
|
1063
|
+
recycledLeftBehind?: LeftBehind;
|
|
1064
|
+
/** Why the tree could not be measured before that recycle (git could not
|
|
1065
|
+
* read it) — still discarded with the disk, never recorded as clean. */
|
|
1066
|
+
recycledUnmeasured?: string;
|
|
1040
1067
|
/** How deps were last materialized (evidence that the per-branch reconciliation ran). */
|
|
1041
1068
|
deps?: ThreadDepsMechanism;
|
|
1042
1069
|
/** Commit the worktree was last attached at (the ref's tip in the mirror
|
|
@@ -1111,9 +1138,10 @@ interface AttachOk {
|
|
|
1111
1138
|
* and why — so the bot can say where the follow-up runs and why. */
|
|
1112
1139
|
rebindRefused?: RebindRefused;
|
|
1113
1140
|
/** This attach moved the binding BACK to the default branch (item 16's
|
|
1114
|
-
* second movement): the branch
|
|
1115
|
-
* mirror, so the tree was provisioned on
|
|
1116
|
-
* failing `unknown-ref`. From where, to
|
|
1141
|
+
* second movement): the branch it was on — a rebind's, or one this thread
|
|
1142
|
+
* itself pushed — is gone from the mirror, so the tree was provisioned on
|
|
1143
|
+
* the default instead of the attach failing `unknown-ref`. From where, to
|
|
1144
|
+
* where, which PR's branch it was. */
|
|
1117
1145
|
returned?: Returned;
|
|
1118
1146
|
}
|
|
1119
1147
|
|
|
@@ -2083,13 +2111,20 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2083
2111
|
throw new StepError("cat-file", `git cat-file exited ${r.exitCode}: ${r.stderr.trim() || "(no output)"}`);
|
|
2084
2112
|
}
|
|
2085
2113
|
|
|
2086
|
-
/** Attach's fetch decision (item 51) over the mirror's actual state
|
|
2087
|
-
* is
|
|
2088
|
-
*
|
|
2089
|
-
|
|
2114
|
+
/** Attach's fetch decision (item 51) over the mirror's actual state — why
|
|
2115
|
+
* the mirror is fetched before the tree is cloned, or null: the ref is
|
|
2116
|
+
* missing, `wantSha` names a commit the ref's tip is not at, or the ref is
|
|
2117
|
+
* one the binding could return from (item 16's second movement) and is
|
|
2118
|
+
* verified against the origin before the mirror's word on it is trusted.
|
|
2119
|
+
* The decision itself is the pure, tested `mirrorFetchReason`. */
|
|
2120
|
+
private async mirrorFetchReasonFor(
|
|
2121
|
+
ref: string,
|
|
2122
|
+
wantSha: string | null,
|
|
2123
|
+
returnable: boolean,
|
|
2124
|
+
): Promise<FetchReason | null> {
|
|
2090
2125
|
const refExists = await this.refExists(ref);
|
|
2091
2126
|
const mirrorSha = refExists && wantSha !== null ? await this.readMirrorSha(ref) : undefined;
|
|
2092
|
-
return
|
|
2127
|
+
return mirrorFetchReason({ refExists, mirrorSha, wantSha, returnable });
|
|
2093
2128
|
}
|
|
2094
2129
|
|
|
2095
2130
|
/** Dependency/build cache key: sha256 over the ls-tree lines (mode,
|
|
@@ -2945,12 +2980,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2945
2980
|
throw new CycleRestartError("image-stale-restart");
|
|
2946
2981
|
}
|
|
2947
2982
|
|
|
2948
|
-
// Idle sleep:
|
|
2949
|
-
//
|
|
2983
|
+
// Idle sleep: no live binding used within IDLE_AFTER_S and nothing in
|
|
2984
|
+
// flight → skip this fetch; the cron holds the next instance to the idle
|
|
2950
2985
|
// cadence, so SLEEP_AFTER can elapse. Staleness is repaid at the next
|
|
2951
|
-
// attach (refreshIfStale).
|
|
2952
|
-
//
|
|
2953
|
-
// uncommitted work is not snapshotted.
|
|
2986
|
+
// attach (refreshIfStale). What a live tree holds is not asked (item 17):
|
|
2987
|
+
// sleep destroys the disk, and a tree no run is using protects nothing.
|
|
2954
2988
|
// Only a SETTLED resident may park: a cycle that finds `refreshing`/
|
|
2955
2989
|
// `restoring` at entry is looking at a marker left by a cycle that died
|
|
2956
2990
|
// mid-flight (a deploy evicting the DO: stuck `refreshing` + parked →
|
|
@@ -3002,7 +3036,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3002
3036
|
await this.ctx.storage.delete(DEGRADED_STREAK_KEY);
|
|
3003
3037
|
}
|
|
3004
3038
|
if (settled && (await this.isIdle())) {
|
|
3005
|
-
// isIdle awaited (
|
|
3039
|
+
// isIdle awaited (the bindings listing) — re-read before writing.
|
|
3006
3040
|
const now = (await this.ctx.storage.get<RepoFacts>(FACTS_KEY)) ?? facts;
|
|
3007
3041
|
if (!now.idleSince)
|
|
3008
3042
|
await this.ctx.storage.put(FACTS_KEY, {
|
|
@@ -3798,15 +3832,24 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3798
3832
|
});
|
|
3799
3833
|
}
|
|
3800
3834
|
|
|
3801
|
-
/**
|
|
3802
|
-
*
|
|
3803
|
-
*
|
|
3804
|
-
*
|
|
3805
|
-
*
|
|
3806
|
-
|
|
3807
|
-
|
|
3835
|
+
/** Whether a run may be using this disk: a live binding attached to or
|
|
3836
|
+
* used (an exec bumps `lastAttachAt` too, item 21) within IDLE_AFTER_S.
|
|
3837
|
+
* The op counter is 0 between a run's tool calls, so this floor is what
|
|
3838
|
+
* stands for a run mid-flight. The one predicate behind "the disk may go
|
|
3839
|
+
* away": the idle-sleep gate (item 16b — a platform sleep destroys the
|
|
3840
|
+
* disk) and the disk-full recycle (item 54) both read it, and neither asks
|
|
3841
|
+
* what a tree holds (item 17): a dirty tree no run is using protects
|
|
3842
|
+
* nothing — the next attach wipes it. Bindings are storage, so this needs
|
|
3843
|
+
* no container. */
|
|
3844
|
+
private recentlyUsed(live: ThreadBinding[]): boolean {
|
|
3808
3845
|
const recent = systemClock() - IDLE_AFTER_S * 1000;
|
|
3809
|
-
|
|
3846
|
+
return live.some((b) => Date.parse(b.lastAttachAt) >= recent);
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
/** Idle = no live binding used within the floor and nothing in flight; the
|
|
3850
|
+
* sweep's release records what an idle tree held. */
|
|
3851
|
+
private async isIdle(): Promise<boolean> {
|
|
3852
|
+
if (this.recentlyUsed(await this.liveBindings())) return false;
|
|
3810
3853
|
return this.inFlightCount() === 0;
|
|
3811
3854
|
}
|
|
3812
3855
|
|
|
@@ -3816,17 +3859,6 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3816
3859
|
return [...all.values()].filter((b) => !b.evicted && b.user);
|
|
3817
3860
|
}
|
|
3818
3861
|
|
|
3819
|
-
/** The disk-full recycle's tree guard (item 54), its one reader: true when
|
|
3820
|
-
* every live tree is clean as its thread user, or the runtime is already
|
|
3821
|
-
* down — a sleep has destroyed the disk, so there is nothing left to lose. */
|
|
3822
|
-
private async liveTreesClean(live: ThreadBinding[]): Promise<boolean> {
|
|
3823
|
-
if (!(await this.isRuntimeActive().catch(() => false))) return true;
|
|
3824
|
-
// Concurrent: each check touches only its own (disjoint) tree, and one
|
|
3825
|
-
// spawn each — no serial 3-probe round-trip per live binding.
|
|
3826
|
-
const checks = await Promise.all(live.map((b) => this.worktreeCleanliness(b)));
|
|
3827
|
-
return checks.every((c) => c.clean); // any dirty or unknown → not clean
|
|
3828
|
-
}
|
|
3829
|
-
|
|
3830
3862
|
// -- disk-full (docs/reference/specs/resident-repos.md item 54) ---------------------------
|
|
3831
3863
|
|
|
3832
3864
|
/** Free space on the workspace mount in KiB; `null` when df cannot answer.
|
|
@@ -3852,28 +3884,54 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3852
3884
|
|
|
3853
3885
|
/** The disk is a cache: stop the container so the next cycle attempt
|
|
3854
3886
|
* restores mirror + checkout from R2 onto an empty disk — the same wake
|
|
3855
|
-
* path as a platform sleep. Only when the pure plan allows it:
|
|
3856
|
-
* flight (`selfInFlight` excludes the
|
|
3857
|
-
*
|
|
3858
|
-
*
|
|
3859
|
-
*
|
|
3860
|
-
*
|
|
3887
|
+
* path as a platform sleep. Only when the pure plan allows it: no recycle
|
|
3888
|
+
* within the cooldown, nothing in flight (`selfInFlight` excludes the
|
|
3889
|
+
* calling refresh cycle from the count) and no live binding used within
|
|
3890
|
+
* the idle floor — the idle-sleep gate's own predicate (`recentlyUsed`),
|
|
3891
|
+
* because a sleep and a recycle destroy the same disk: it may go away when
|
|
3892
|
+
* no run is using it, never for what the trees hold (item 17). A recycle
|
|
3893
|
+
* discards every live tree the way an eviction does, so each is measured
|
|
3894
|
+
* first, as its thread user, and its binding records what went
|
|
3895
|
+
* (`recordRecycledTree`); the plan is decided again after those awaits,
|
|
3896
|
+
* right before the stop. A refused recycle is written to
|
|
3897
|
+
* `lastRefreshError` with its why, so `/residents` says what an operator
|
|
3898
|
+
* must do; the `degraded` reason stays the clean `disk-full: …`. Answers
|
|
3899
|
+
* whether the container was recycled. */
|
|
3861
3900
|
private async recoverFromDiskFull(reason: string, selfInFlight: number): Promise<boolean> {
|
|
3862
3901
|
const lastRecycleAt = await this.ctx.storage.get<number>(DISK_FULL_RECYCLE_KEY);
|
|
3863
|
-
const plan =
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3902
|
+
const plan = (live: ThreadBinding[]) =>
|
|
3903
|
+
planDiskFullRecovery({
|
|
3904
|
+
now: systemClock(),
|
|
3905
|
+
lastRecycleAt,
|
|
3906
|
+
inFlight: this.inFlightCount() - selfInFlight,
|
|
3907
|
+
recentlyUsed: this.recentlyUsed(live),
|
|
3908
|
+
idleFloorS: IDLE_AFTER_S,
|
|
3909
|
+
});
|
|
3910
|
+
const kept = async (why: string) => {
|
|
3911
|
+
console.log(`disk-full: container kept — ${why}`);
|
|
3912
|
+
await this.recordRefreshError(`${reason} — container kept: ${why}`);
|
|
3872
3913
|
return false;
|
|
3873
|
-
}
|
|
3914
|
+
};
|
|
3915
|
+
const live = await this.liveBindings();
|
|
3916
|
+
const first = plan(live);
|
|
3917
|
+
if (first.action === "wait") return kept(first.why);
|
|
3918
|
+
// What each live tree holds, for its record — never a reason to keep the
|
|
3919
|
+
// container. Concurrent: each probe touches only its own tree, one spawn
|
|
3920
|
+
// each. Runtime down: the disk, and every tree with it, is already gone.
|
|
3921
|
+
const active = await this.isRuntimeActive().catch(() => false);
|
|
3922
|
+
const trees = active
|
|
3923
|
+
? await Promise.all(
|
|
3924
|
+
live.map(async (binding) => [binding, await this.measureTreeBeforeEviction(binding)] as const),
|
|
3925
|
+
)
|
|
3926
|
+
: live.map((binding) => [binding, undefined] as const);
|
|
3927
|
+
// The measurement awaited (the DO yields at each await): decide again over
|
|
3928
|
+
// fresh facts — an attach or an op that landed meanwhile keeps the container.
|
|
3929
|
+
const verdict = plan(await this.liveBindings());
|
|
3930
|
+
if (verdict.action === "wait") return kept(verdict.why);
|
|
3874
3931
|
console.log(
|
|
3875
3932
|
`disk-full: recycling the container — the next cycle restores mirror + checkout from R2 onto an empty disk (${reason})`,
|
|
3876
3933
|
);
|
|
3934
|
+
for (const [binding, tree] of trees) await this.recordRecycledTree(binding, tree);
|
|
3877
3935
|
await this.ctx.storage.put(DISK_FULL_RECYCLE_KEY, systemClock());
|
|
3878
3936
|
await this.recordRefreshError(`${reason} — container recycled; restoring from R2 on the next cycle`);
|
|
3879
3937
|
this.swapIncarnation(); // deliberate incarnation swap
|
|
@@ -3881,6 +3939,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3881
3939
|
return true;
|
|
3882
3940
|
}
|
|
3883
3941
|
|
|
3942
|
+
/** The disk-full recycle's record for one live tree (item 54), written the
|
|
3943
|
+
* way `evictBinding` writes an eviction's — but the binding stays live, its
|
|
3944
|
+
* pool user kept and its tree recreated on the next attach, so this is not
|
|
3945
|
+
* an eviction and the record sits under its own name beside
|
|
3946
|
+
* `evictedLeftBehind`: the counts when something was there, the probe's
|
|
3947
|
+
* first error line when git could not read the tree, both rewritten on
|
|
3948
|
+
* every recycle so nothing lingers from an earlier one, and one log line
|
|
3949
|
+
* when the tree held something. Re-read first: a binding evicted or
|
|
3950
|
+
* re-attached during the measurement is left as is. */
|
|
3951
|
+
private async recordRecycledTree(binding: ThreadBinding, tree: EvictedTree | undefined): Promise<void> {
|
|
3952
|
+
const now = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(binding.threadKey));
|
|
3953
|
+
if (!now || now.evicted || now.lastAttachAt !== binding.lastAttachAt) return;
|
|
3954
|
+
await this.ctx.storage.put(threadBindingKey(binding.threadKey), {
|
|
3955
|
+
...now,
|
|
3956
|
+
recycledLeftBehind: tree && "leftBehind" in tree ? tree.leftBehind : undefined,
|
|
3957
|
+
recycledUnmeasured: tree && "unmeasured" in tree ? tree.unmeasured : undefined,
|
|
3958
|
+
} satisfies ThreadBinding);
|
|
3959
|
+
if (tree)
|
|
3960
|
+
console.log(`disk-full: ${binding.threadKey} tree discarded with the disk — ${evictedTreeSentence(tree)}`);
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3884
3963
|
// -- disk budget (docs/reference/specs/resident-repos.md item 55) -------------------------
|
|
3885
3964
|
|
|
3886
3965
|
/** The `df` half: total/used/free of the workspace mount; null when df
|
|
@@ -4012,12 +4091,36 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4012
4091
|
committedKiB: this.diskCommittedKiB,
|
|
4013
4092
|
diskBudgetMb: input.record.diskBudgetMb,
|
|
4014
4093
|
kind,
|
|
4094
|
+
// A deps-store entry backup stages its archive from the deps (item
|
|
4095
|
+
// 61); the reserve holds their share only while one runs.
|
|
4096
|
+
depsBackupInFlight: this.depsBackupsInFlight.size > 0,
|
|
4015
4097
|
});
|
|
4016
4098
|
let rawFree = df.freeKiB;
|
|
4017
4099
|
let verdict = decide(rawFree);
|
|
4018
4100
|
const evicted: Array<{ threadKey: string; freedKiB: number | null }> = [];
|
|
4019
4101
|
// Item 62: keep decisions travel as tokens; the key stays for the log line.
|
|
4020
4102
|
const kept: Array<{ threadKey: string; why: DiskKeepWhy }> = [];
|
|
4103
|
+
// The deps store's spares go first (item 55): a warm cache no live tree
|
|
4104
|
+
// references, worth minutes on a future attach, against a tree refused
|
|
4105
|
+
// now. Their bytes come back before any idle tree is considered.
|
|
4106
|
+
let spares: Array<{ key: string; freedKiB: number }> = [];
|
|
4107
|
+
if (!verdict.fits) {
|
|
4108
|
+
spares = await this.evictDepsSparesUnderPressure().catch((err) => {
|
|
4109
|
+
console.log(`disk-pressure: deps spare eviction failed: ${errMsg(err)}`);
|
|
4110
|
+
return [];
|
|
4111
|
+
});
|
|
4112
|
+
if (spares.length > 0) {
|
|
4113
|
+
console.log(
|
|
4114
|
+
`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}`,
|
|
4115
|
+
);
|
|
4116
|
+
rawFree = rawFreeAfterEviction(
|
|
4117
|
+
rawFree,
|
|
4118
|
+
await this.dfSample(),
|
|
4119
|
+
spares.reduce((a, e) => a + e.freedKiB, 0),
|
|
4120
|
+
);
|
|
4121
|
+
verdict = decide(rawFree);
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4021
4124
|
if (!verdict.fits) {
|
|
4022
4125
|
const live = await this.liveBindings();
|
|
4023
4126
|
const candidates: DiskEvictionCandidate[] = live.map((b) => ({
|
|
@@ -4064,7 +4167,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4064
4167
|
// truthiness does not narrow a boolean-literal discriminant.
|
|
4065
4168
|
const final = verdict;
|
|
4066
4169
|
if (final.fits === false) {
|
|
4067
|
-
const reason = diskPressureReason({ verdict: final, evicted, kept });
|
|
4170
|
+
const reason = diskPressureReason({ verdict: final, evicted, kept, spares });
|
|
4068
4171
|
console.log(`attach ${input.threadKey}: ${reason}`);
|
|
4069
4172
|
const s = await this.getStatus();
|
|
4070
4173
|
return { error: reason, status: 503, state: s.state, reason: DISK_PRESSURE_REASON };
|
|
@@ -4072,7 +4175,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4072
4175
|
const m = final.math;
|
|
4073
4176
|
console.log(
|
|
4074
4177
|
`attach ${input.threadKey}: disk admitted — ${kind} ${formatGiB(m.projectedKiB)} projected, ${formatGiB(m.freeKiB)} free, ` +
|
|
4075
|
-
`reserve ${formatGiB(m.reserve.totalKiB)}, headroom ${formatGiB(m.headroomKiB)}${evicted.length > 0 ? `, evicted ${evicted.map((e) => e.threadKey).join(", ")}` : ""}`,
|
|
4178
|
+
`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(", ")}` : ""}`,
|
|
4076
4179
|
);
|
|
4077
4180
|
const committedKiB = m.projectedKiB ?? 0;
|
|
4078
4181
|
this.diskCommittedKiB += committedKiB;
|
|
@@ -4406,12 +4509,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4406
4509
|
lastAttachAt: now,
|
|
4407
4510
|
evicted: false,
|
|
4408
4511
|
// An evicted binding's own history: how its ref was chosen (absent on
|
|
4409
|
-
// one made before the field — never overwritten by this attach's flag)
|
|
4410
|
-
//
|
|
4512
|
+
// one made before the field — never overwritten by this attach's flag),
|
|
4513
|
+
// the one move it may have made and its last move back.
|
|
4411
4514
|
...(existing
|
|
4412
4515
|
? {
|
|
4413
4516
|
...(existing.boundBy !== undefined ? { boundBy: existing.boundBy } : {}),
|
|
4414
4517
|
...(existing.rebound !== undefined ? { rebound: existing.rebound } : {}),
|
|
4518
|
+
...(existing.returned !== undefined ? { returned: existing.returned } : {}),
|
|
4415
4519
|
...(existing.ownBranches !== undefined ? { ownBranches: existing.ownBranches } : {}),
|
|
4416
4520
|
}
|
|
4417
4521
|
: { boundBy }),
|
|
@@ -4764,27 +4868,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4764
4868
|
|
|
4765
4869
|
/** Item 16's second movement, decided where the fact is established — under
|
|
4766
4870
|
* the mirror mutex, after the attach's fetch found the bound ref gone: a
|
|
4767
|
-
* binding a
|
|
4768
|
-
*
|
|
4769
|
-
*
|
|
4770
|
-
*
|
|
4771
|
-
*
|
|
4772
|
-
*
|
|
4773
|
-
*
|
|
4774
|
-
*
|
|
4775
|
-
*
|
|
4871
|
+
* binding on a branch that is now deleted (its pull request merged) goes
|
|
4872
|
+
* back to the default, so the run starts clean there (item 17) instead of
|
|
4873
|
+
* the thread failing `unknown-ref` for the rest of its life — when the
|
|
4874
|
+
* branch is one a rebind moved it onto, or one this thread's own runs
|
|
4875
|
+
* pushed as the binding remembers it (`ownBranches`): a ship unit's coding
|
|
4876
|
+
* child binds its unit branch by name and pushes it, and the merge deletes
|
|
4877
|
+
* it. The row as it stands is re-read and re-judged (the pure
|
|
4878
|
+
* `returnToDefault`, which writes nothing for a row `canReturnToDefault`
|
|
4879
|
+
* does not admit: a ref a person named that the thread never pushed never
|
|
4880
|
+
* returns — that branch is theirs to sort out), the move recorded so the
|
|
4881
|
+
* thread may follow its next pull request, and the answer carries the move
|
|
4882
|
+
* back for the card. A row that no longer qualifies — another attach moved
|
|
4883
|
+
* it meanwhile — is answered as it stands, nothing written. */
|
|
4776
4884
|
private async returnBindingToDefault(
|
|
4777
4885
|
binding: ThreadBinding,
|
|
4778
4886
|
defaultRef: string,
|
|
4779
4887
|
): Promise<{ binding: ThreadBinding; returned?: Returned }> {
|
|
4780
4888
|
const key = threadBindingKey(binding.threadKey);
|
|
4781
4889
|
const current = (await this.ctx.storage.get<ThreadBinding>(key)) ?? binding;
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
{ ...current, rebound: current.rebound },
|
|
4785
|
-
defaultRef,
|
|
4786
|
-
new Date(systemClock()).toISOString(),
|
|
4787
|
-
);
|
|
4890
|
+
const back = returnToDefault(current, defaultRef, new Date(systemClock()).toISOString());
|
|
4891
|
+
if (back === undefined) return { binding: current };
|
|
4788
4892
|
await this.ctx.storage.put(key, back.binding);
|
|
4789
4893
|
console.log(
|
|
4790
4894
|
`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`,
|
|
@@ -4866,9 +4970,22 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4866
4970
|
// The expected head applies to the ref it was resolved for; a sticky
|
|
4867
4971
|
// binding on another branch drops it (item 51) rather than fetching on
|
|
4868
4972
|
// every attach of a thread that can never be at that commit.
|
|
4973
|
+
// A ref the binding could return from — a branch a rebind moved it onto,
|
|
4974
|
+
// or one its own runs pushed (item 16's second movement) — is fetched for
|
|
4975
|
+
// whether or not the mirror holds it: the branch dies when its pull
|
|
4976
|
+
// request merges, the deletion reaches the mirror only through a
|
|
4977
|
+
// `fetch --prune`, and a mirror trusted for still holding the ref would
|
|
4978
|
+
// provision a tree at the deleted branch's stale tip until the refresh
|
|
4979
|
+
// cycle's prune caught up. One predicate, read once here, drives the mint
|
|
4980
|
+
// pre-check, the fetch under the lock and the return gate alike.
|
|
4869
4981
|
let want = wantShaForBinding({ boundRef: binding.ref, refHint, wantSha });
|
|
4982
|
+
const returnable = canReturnToDefault(binding, facts.defaultRef);
|
|
4870
4983
|
let fetchToken: string | null = token;
|
|
4871
|
-
if (
|
|
4984
|
+
if (
|
|
4985
|
+
!fetchToken &&
|
|
4986
|
+
githubAppConfigured(this.env) &&
|
|
4987
|
+
(await this.mirrorFetchReasonFor(binding.ref, want, returnable)) !== null
|
|
4988
|
+
) {
|
|
4872
4989
|
fetchToken = (await mintRepoScopedToken(this.env, slug).catch(() => null))?.token ?? null;
|
|
4873
4990
|
}
|
|
4874
4991
|
|
|
@@ -4876,14 +4993,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4876
4993
|
try {
|
|
4877
4994
|
locked = await this.withMirrorLock(async () => {
|
|
4878
4995
|
await this.ensureGitSetup();
|
|
4879
|
-
const
|
|
4880
|
-
if (
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4996
|
+
const why = await this.mirrorFetchReasonFor(binding.ref, want, returnable);
|
|
4997
|
+
if (why !== null) {
|
|
4998
|
+
try {
|
|
4999
|
+
await this.gitWithCred(
|
|
5000
|
+
fetchToken,
|
|
5001
|
+
["-C", MIRROR_DIR, "fetch", "--prune", "origin"],
|
|
5002
|
+
"fetch",
|
|
5003
|
+
GIT_NETWORK_TIMEOUT_MS,
|
|
5004
|
+
);
|
|
5005
|
+
} catch (err) {
|
|
5006
|
+
// A fetch for a missing or stale ref fails the attach as it always
|
|
5007
|
+
// did. One that only verifies a returnable ref the mirror holds is
|
|
5008
|
+
// not the attach's failure — the origin unreachable, no token for
|
|
5009
|
+
// a private repo — so the attach goes on with the mirror's ref, as
|
|
5010
|
+
// it did before the verification existed; the return is only
|
|
5011
|
+
// ever decided on a fetch that ran.
|
|
5012
|
+
if (why !== "returnable-ref") throw err;
|
|
5013
|
+
console.log(
|
|
5014
|
+
`attach ${threadKey}: the fetch verifying ${binding.ref} at the origin failed (${errMsg(err)}) — the attach goes on with the mirror's ref`,
|
|
5015
|
+
);
|
|
5016
|
+
}
|
|
4887
5017
|
}
|
|
4888
5018
|
// What to check out, now that the mirror is as fresh as it will get
|
|
4889
5019
|
// (item 51, the pure `attachTarget`): the ref's tip when the ref is
|
|
@@ -4892,18 +5022,21 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4892
5022
|
// `refs/pull/N/head` still names its head, and the caller told us that
|
|
4893
5023
|
// head — so a review of a merged PR stays on the warm resident instead
|
|
4894
5024
|
// of falling back to a cold sandbox that clones the same commit itself.
|
|
4895
|
-
// No fetch
|
|
4896
|
-
// for every missing ref, so only a fetched mirror needs the
|
|
4897
|
-
//
|
|
4898
|
-
|
|
5025
|
+
// No fetch was due ⇒ the ref existed at the check: `mirrorFetchReason`
|
|
5026
|
+
// fetches for every missing ref, so only a fetched mirror needs the
|
|
5027
|
+
// re-read — and a returnable ref is re-read for real, the fact the
|
|
5028
|
+
// second movement is decided on. (Under the mirror mutex — the mirror
|
|
5029
|
+
// cannot change in between.)
|
|
5030
|
+
const refExists = why === null || (await this.refExists(binding.ref));
|
|
4899
5031
|
let target = attachTarget({
|
|
4900
5032
|
refExists,
|
|
4901
5033
|
wantSha: want,
|
|
4902
5034
|
commitInMirror: !refExists && want !== null && (await this.commitInMirror(want)),
|
|
4903
5035
|
});
|
|
4904
|
-
if (target.kind === "unknown-ref" &&
|
|
4905
|
-
// The branch a rebind moved
|
|
4906
|
-
//
|
|
5036
|
+
if (target.kind === "unknown-ref" && returnable) {
|
|
5037
|
+
// The thread's branch — one a rebind moved it onto, or one its own
|
|
5038
|
+
// runs pushed and it was bound to by name — is gone even after the
|
|
5039
|
+
// fetch: deleted once its pull request merged. The binding goes
|
|
4907
5040
|
// back to the default (item 16's second movement) and the attach
|
|
4908
5041
|
// goes on there: the default is always in the mirror, and the
|
|
4909
5042
|
// worktree step below provisions the tree at its tip.
|
|
@@ -5658,27 +5791,24 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5658
5791
|
* in flight, and remove what `planDepsEviction` names — debris first, then
|
|
5659
5792
|
* the coldest spares beyond DEPS_STORE_MAX_UNREFERENCED. Housekeeping:
|
|
5660
5793
|
* a failure is a log line, never a lifecycle flip. */
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
const listing = parseDepsStoreListing(listed.stdout);
|
|
5665
|
-
if (listing.entries.length === 0 && listing.leftovers.length === 0) return;
|
|
5794
|
+
/** The store entries no eviction may touch: the checkout's key, every live
|
|
5795
|
+
* binding's, every install and every entry backup in flight. */
|
|
5796
|
+
private async depsProtectedKeys(): Promise<Set<string>> {
|
|
5666
5797
|
const facts = await this.ctx.storage.get<RepoFacts>(FACTS_KEY);
|
|
5667
5798
|
const protectedKeys = new Set<string>([...this.depsInFlight.keys(), ...this.depsBackupsInFlight]);
|
|
5668
5799
|
if (facts?.lockfileHash) protectedKeys.add(facts.lockfileHash);
|
|
5669
5800
|
for (const b of await this.liveBindings()) if (b.depsKey) protectedKeys.add(b.depsKey);
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5801
|
+
return protectedKeys;
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
/** Remove the store entries a plan names and drop their entry backups with
|
|
5805
|
+
* them (item 61 PR B: a spare nothing references on disk is a spare nothing
|
|
5806
|
+
* will wake into). Shared by the sweep and the attach's pressure path. */
|
|
5807
|
+
private async applyDepsEviction(plan: DepsEvictionPlan, listing: DepsStoreListing, who: string): Promise<void> {
|
|
5676
5808
|
await this.runOk(["rm", "-rf", ...plan.remove], "deps-evict", { timeoutMs: DU_TIMEOUT_MS });
|
|
5677
5809
|
console.log(
|
|
5678
|
-
`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(", ")}`,
|
|
5810
|
+
`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(", ")}`,
|
|
5679
5811
|
);
|
|
5680
|
-
// The evicted entries' archives go with them (item 61 PR B): a spare
|
|
5681
|
-
// nothing references on disk is a spare nothing will wake into.
|
|
5682
5812
|
const kept = new Set(plan.keep);
|
|
5683
5813
|
const evictedKeys = listing.entries.map((e) => e.key).filter((k) => !kept.has(k));
|
|
5684
5814
|
const drop = depsBackupsToDrop({
|
|
@@ -5693,6 +5823,44 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5693
5823
|
}
|
|
5694
5824
|
}
|
|
5695
5825
|
|
|
5826
|
+
/** Under disk pressure (item 55) the store keeps no spare: every complete
|
|
5827
|
+
* entry no live tree references goes, coldest first, before any idle tree
|
|
5828
|
+
* is considered — a spare is a cache worth minutes on a future attach, a
|
|
5829
|
+
* refused tree is a run falling to a cold sandbox now. Leftovers are the
|
|
5830
|
+
* sweep's business (they may be live installs' scratch). Answers what it
|
|
5831
|
+
* removed with the bytes each measured at; a listing that fails answers
|
|
5832
|
+
* nothing and the tree path decides as before. */
|
|
5833
|
+
private async evictDepsSparesUnderPressure(): Promise<Array<{ key: string; freedKiB: number }>> {
|
|
5834
|
+
const listed = await this.run(["sh", "-c", depsStoreListScript()], { timeoutMs: DU_TIMEOUT_MS });
|
|
5835
|
+
if (listed.exitCode !== 0) return [];
|
|
5836
|
+
const listing = parseDepsStoreListing(listed.stdout);
|
|
5837
|
+
if (listing.entries.length === 0) return [];
|
|
5838
|
+
const plan = planDepsEviction({
|
|
5839
|
+
entries: listing.entries,
|
|
5840
|
+
leftovers: [],
|
|
5841
|
+
protectedKeys: await this.depsProtectedKeys(),
|
|
5842
|
+
maxUnreferenced: DEPS_STORE_MAX_UNREFERENCED_UNDER_PRESSURE,
|
|
5843
|
+
});
|
|
5844
|
+
if (plan.remove.length === 0) return [];
|
|
5845
|
+
await this.applyDepsEviction(plan, listing, "disk-pressure");
|
|
5846
|
+
return plan.evicted.map((e) => ({ key: e.key, freedKiB: e.kib }));
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
private async sweepDepsStore(): Promise<void> {
|
|
5850
|
+
const listed = await this.run(["sh", "-c", depsStoreListScript()], { timeoutMs: DU_TIMEOUT_MS });
|
|
5851
|
+
if (listed.exitCode !== 0) return;
|
|
5852
|
+
const listing = parseDepsStoreListing(listed.stdout);
|
|
5853
|
+
if (listing.entries.length === 0 && listing.leftovers.length === 0) return;
|
|
5854
|
+
const protectedKeys = await this.depsProtectedKeys();
|
|
5855
|
+
// A scratch/staging dir of an install still running in this incarnation
|
|
5856
|
+
// is live work, not debris; the listing cannot tell them apart, so
|
|
5857
|
+
// leftovers wait for a sweep with nothing in flight.
|
|
5858
|
+
const leftovers = this.depsInFlight.size > 0 ? [] : listing.leftovers;
|
|
5859
|
+
const plan = planDepsEviction({ entries: listing.entries, leftovers, protectedKeys });
|
|
5860
|
+
if (plan.remove.length === 0) return;
|
|
5861
|
+
await this.applyDepsEviction(plan, listing, "sweep");
|
|
5862
|
+
}
|
|
5863
|
+
|
|
5696
5864
|
/** The per-user 700 staging dir (`install -d` is idempotent), memoized per
|
|
5697
5865
|
* incarnation — the write/credential paths used to fork for it on every
|
|
5698
5866
|
* call. Cleared with the other memos; a fresh disk simply re-creates it. */
|
|
@@ -6202,11 +6370,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6202
6370
|
* (su), never as root: the worktree is thread-owned, so root git in it
|
|
6203
6371
|
* would be refused by safe.directory and would be the exact repo-local-
|
|
6204
6372
|
* config execution vector safe.directory exists to block. Read by
|
|
6205
|
-
* `measureTreeBeforeEviction` for the
|
|
6206
|
-
*
|
|
6207
|
-
*
|
|
6208
|
-
*
|
|
6209
|
-
* measured: there was nothing to discard.
|
|
6373
|
+
* `measureTreeBeforeEviction` alone, for the record of an eviction or of
|
|
6374
|
+
* the disk-full recycle — never a reason to keep a tree or a container
|
|
6375
|
+
* (item 17). A tree that no longer exists (disk recycled by a sleep/wake)
|
|
6376
|
+
* is clean with nothing measured: there was nothing to discard.
|
|
6210
6377
|
*
|
|
6211
6378
|
* ONE spawn: the presence test and both git probes fold
|
|
6212
6379
|
* into the pure `worktreeCleanlinessScript` (test -d as root, both git
|
|
@@ -6222,7 +6389,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6222
6389
|
return parseWorktreeCleanliness(r);
|
|
6223
6390
|
}
|
|
6224
6391
|
|
|
6225
|
-
/** What a tree holds right before its eviction
|
|
6392
|
+
/** What a tree holds right before its eviction — or before the disk-full
|
|
6393
|
+
* recycle discards it with the disk (item 54) — for the record and the log
|
|
6226
6394
|
* (item 17): the one-spawn probe as the thread user, read once into
|
|
6227
6395
|
* `evictedTreeOf` — never into a keep decision. Callers skip it when the
|
|
6228
6396
|
* runtime is down: the disk, and the tree with it, is already gone. */
|
|
@@ -6782,6 +6950,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6782
6950
|
evictedWhy,
|
|
6783
6951
|
evictedLeftBehind,
|
|
6784
6952
|
evictedUnmeasured,
|
|
6953
|
+
recycledLeftBehind,
|
|
6954
|
+
recycledUnmeasured,
|
|
6785
6955
|
}) => ({
|
|
6786
6956
|
threadKey,
|
|
6787
6957
|
ref,
|
|
@@ -6795,6 +6965,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6795
6965
|
evictedWhy: evictedWhy ?? null,
|
|
6796
6966
|
evictedLeftBehind: evictedLeftBehind ?? null,
|
|
6797
6967
|
evictedUnmeasured: evictedUnmeasured ?? null,
|
|
6968
|
+
recycledLeftBehind: recycledLeftBehind ?? null,
|
|
6969
|
+
recycledUnmeasured: recycledUnmeasured ?? null,
|
|
6798
6970
|
}),
|
|
6799
6971
|
);
|
|
6800
6972
|
return {
|