@coreplane/switchboard 1.234.0 → 1.236.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/gc.ts +6 -9
- package/dist/assets/deploy/cloudflare-resident/worker.ts +225 -119
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +143 -13
- 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/coordinator/contract.ts +24 -4
- package/dist/assets/src/core/coordinator/driver.ts +31 -3
- package/dist/assets/src/core/runEvents.ts +43 -1
- 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 +213 -29
- package/dist/assets/src/core/trace/types.ts +5 -0
- package/dist/assets/src/execution/residentCleanliness.ts +24 -0
- package/dist/assets/src/execution/residentDisk.ts +21 -10
- package/dist/assets/src/execution/residentDiskBudget.ts +7 -6
- package/dist/assets/src/execution/sandboxErrors.ts +77 -0
- package/dist/assets/src/execution/sandboxIdle.ts +220 -0
- package/dist/assets/src/execution/sandboxStart.ts +90 -0
- package/dist/assets/web/dist/.vite/manifest.json +437 -422
- package/dist/assets/web/dist/assets/{AppShell-lYVcj-k7.js → AppShell-DZdz_sl7.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-D-v8am88.js → CostsPage-BfJsol9y.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-CvlWP7Eq.js → DeliveryPage-CvInfu7y.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-BzWS4Ca1.js → NotFoundPage-d37EP8hS.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DQSD0SZD.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CZUGLen6.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-Bngn4GjF.js +9 -0
- package/dist/assets/web/dist/assets/RunRoutePage-a-tMrirB.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-s0VKmRda.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-DEICQ4FY.js → RunsTabs-CyCdOhJ2.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-ByCmyrDs.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DhKEokGK.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-Ct6AtHhS.js → StatusDot-D-BPSPpw.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-qol5Boof.js → Tooltip-DPDx5tXT.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-0eiekTHQ.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Cw130Zyi.js → angular-html-Ni1Xil2G.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DY1m4C2s.js → angular-ts-oCgo6p05.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-W6Ty7v05.js → apl-CiWYl0gh.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DlLVHwVk.js → astro-B-VeKZ_J.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-Bco3tDh4.js → blade-BIWEISKd.js} +1 -1
- package/dist/assets/web/dist/assets/{c-1LaFY_cj.js → c-DHxA5Byp.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-B-OvrOL5.js → chapel-D4qGLw-Q.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-Ch3EPlHu.js → cobol-BznX5Q-u.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-hZuITnto.js → coffee-CVr8Idlj.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-B8iRVo8m.js → cpp-wrk6NfW4.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DKveI7lr.js → crystal-CGYWMeH1.js} +1 -1
- package/dist/assets/web/dist/assets/{css-_2_CD-rr.js → css-BgGytMsy.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-DwtJj-ou.js → dist-DSkVB16z.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-DYogEPEk.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BenMDPct.js → edge-CdR6AU9v.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-CBbdEqG6.js → elixir-CQRb3PZx.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DMub_2lY.js → elm-CpuA6W8B.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CI9He_Up.js → erb-CvlrblqH.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-COnovCVc.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-CR9Fxrau.js → glimmer-js-C-DS4g_J.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CwFwX_XS.js → glimmer-ts-DTkorWRI.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Daiodp1r.js → glsl-CH51-JW7.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-DZvrsEkB.js → graphql-BO-XymNv.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BaXOzdg7.js → hack-C-N0oicf.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CMskq_VA.js → haml-BfmdtWoP.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-Dhw_f7jD.js → handlebars-BqhO_te-.js} +1 -1
- package/dist/assets/web/dist/assets/{html-C9k99z0z.js → html-DUEZlTKV.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-VckPItXN.js → html-derivative-DOUJTS8A.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXU_3h9v.js → http-j7UH0Cqv.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-KU24JNdp.js → hurl-D6ZaBLJk.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BjXBzI5Z.js +1 -0
- package/dist/assets/web/dist/assets/{java-BbZjTNgf.js → java-dugWx3wi.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-DXCdqcZZ.js → javascript-CK4GC4JO.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-pDlsjTVp.js → jinja-DrLKIyKG.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DsXDk66e.js → jison-CS1Th-TO.js} +1 -1
- package/dist/assets/web/dist/assets/{json-tCxXfRgG.js → json-CkBqdVys.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-B0ZYkCmi.js → jsx-CCxdg7n3.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-BLzzeI6x.js → julia-BalKW2hA.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DvD2_60c.js → just-BKw1G40D.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-511zn3h7.js → latex-Dfad6ZN-.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BD0BkMox.js → liquid-f_CMGBvd.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-BcMK_uKM.js} +1 -1
- package/dist/assets/web/dist/assets/main-B77vAZVg.css +1 -0
- package/dist/assets/web/dist/assets/main-DR8iKpNt.js +28 -0
- package/dist/assets/web/dist/assets/{marko-1-G_nEXK.js → marko-B0oNLx2i.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DUe3AQLp.js → mdc-Bc_MmIyq.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Dg379_bA.js → nginx-Cmxv8RIt.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BnFq97ZO.js → nim-BtgRfaY0.js} +1 -1
- package/dist/assets/web/dist/assets/{org-F0uiwGvq.js → org-Dl9EzlHh.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-B0euczkl.js → perl-BmF7x7BB.js} +1 -1
- package/dist/assets/web/dist/assets/{php-DkL3k_n7.js → php-rH6CVptQ.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-L_OjjZP4.js → pug-CIN5Ccck.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BMjB00Zz.js → qml-BpLS3RFr.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DoeLdnqR.js → r-BUMF3B-H.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BzYNkAWy.js → razor-B7sTBPdT.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CPmElMk3.js → regexp-h2SvwrsJ.js} +1 -1
- package/dist/assets/web/dist/assets/residentsModel-VSJEepL1.js +1 -0
- package/dist/assets/web/dist/assets/{rst-DrsjgfI7.js → rst-DLOWEsD2.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cB42ppvx.js → ruby-Cl_-I4k3.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-R5Y3NM_I.js → sas-Cm4M-BvV.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-C-zWxV9s.js → scss-CEFCEQUF.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-oJF96aAU.js → shellscript-CE6zb5eS.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CznECKyi.js → shellsession-B4fDVzrM.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-wjHLSRag.js → soy-DwXCMpjE.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-C_BM8IOW.js → sql-DrnAKnyD.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-D31HO8aQ.js → stata-BzlwPWD6.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CKCLyplA.js → surrealql-CeUieZXd.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-3geWk2iu.js → svelte-BERIpIKR.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DiJr_hTD.js → templ-6FSuUDwg.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BEJGWyeF.js → tex-SWCyMwNX.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-C8Mzdpxx.js → ts-tags-VWj7gWgY.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-UQiSm_p3.js → tsx-B6dzfL8L.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-_Pvzkeo9.js → twig-BDXsr482.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BWMkINKK.js → typescript-s100Dt8y.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Bdg9m-e7.js → typst-WfyEAUdt.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CDZrrAZ8.js → vue-CkXOOJ-6.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-tAlpNhfN.js → vue-html-CEpzdcau.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-C27UF_rh.js → vue-vine-D9dsfiFA.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-CxeDr9Zh.js → xml-iBDofbJq.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bpi0uUnM.js → xsl-CqbJF4hD.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BtUgZ1GN.js → yaml-D9i6OxYe.js} +1 -1
- package/dist/cli.js +2336 -1192
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-C_WNBUao.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-D1sTyaZ_.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-C_bcTSjA.js +0 -5
- package/dist/assets/web/dist/assets/RunRoutePage-BWTi_6PG.js +0 -10
- package/dist/assets/web/dist/assets/RunsIndexPage-H6GkSv0a.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-BLcPzm1S.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CIhTEwN7.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-CRtlGxRm.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
|
@@ -116,10 +116,13 @@ import {
|
|
|
116
116
|
type ThreadDepsMechanism,
|
|
117
117
|
} from "../../src/execution/residentDepCache.js";
|
|
118
118
|
import {
|
|
119
|
-
|
|
119
|
+
evictedTreeOf,
|
|
120
|
+
evictedTreeSentence,
|
|
120
121
|
parseWorktreeCleanliness,
|
|
121
122
|
worktreeCleanlinessScript,
|
|
123
|
+
type EvictedTree,
|
|
122
124
|
type LeftBehind,
|
|
125
|
+
type WorktreeCleanliness,
|
|
123
126
|
} from "../../src/execution/residentCleanliness.js";
|
|
124
127
|
import {
|
|
125
128
|
base64LengthOf,
|
|
@@ -502,16 +505,18 @@ const FORCE_DETACH_KILL_TIMEOUT_MS = 2_000;
|
|
|
502
505
|
* binding record is KEPT so the next attach recreates with the same
|
|
503
506
|
* ref. Overridable per resident via the onboard-time `worktreeTtlDays`. */
|
|
504
507
|
const WORKTREE_TTL_DAYS_DEFAULT = 7;
|
|
505
|
-
/** A live binding whose last attach is older than this
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
* release call was lost.
|
|
508
|
+
/** A live binding whose last attach is older than this and has no op in
|
|
509
|
+
* flight is released by the sweep step (every refresh instance runs one),
|
|
510
|
+
* whatever its tree holds (item 17) — runs that ended before /detach existed,
|
|
511
|
+
* or whose release call was lost. */
|
|
509
512
|
const CLEAN_IDLE_RELEASE_S = 60 * 60;
|
|
510
|
-
/** Idle sleep: when no
|
|
511
|
-
*
|
|
512
|
-
* next instance to the idle cadence, so the container can
|
|
513
|
-
* (SLEEP_AFTER); the next attach refreshes first if the mirror
|
|
514
|
-
* (refresh-on-attach).
|
|
513
|
+
/** Idle sleep: when no live binding was attached to or used within this
|
|
514
|
+
* window and nothing is in flight, the refresh cycle skips the fetch and the
|
|
515
|
+
* cron holds the next instance to the idle cadence, so the container can
|
|
516
|
+
* actually sleep (SLEEP_AFTER); the next attach refreshes first if the mirror
|
|
517
|
+
* is stale (refresh-on-attach). The disk-full recycle (item 54) reads the
|
|
518
|
+
* same window through the same predicate (`recentlyUsed`): a sleep and a
|
|
519
|
+
* recycle destroy the same disk, so one rule says when it may go away. */
|
|
515
520
|
const IDLE_AFTER_S = 60 * 60;
|
|
516
521
|
/** LRU eviction floor: an over-cap onboard with `evictColdest:true` may
|
|
517
522
|
* offboard the coldest eligible warm resident, but never one whose last
|
|
@@ -1023,8 +1028,26 @@ interface ThreadBinding {
|
|
|
1023
1028
|
evicted?: boolean;
|
|
1024
1029
|
evictedAt?: string;
|
|
1025
1030
|
/** Why the last eviction happened (the audit trail): `ttl`, `clean-idle`,
|
|
1026
|
-
* `detach`, or a reclamation fate — `merged #N` /
|
|
1031
|
+
* `detach`, `disk-pressure`, or a reclamation fate — `merged #N` /
|
|
1032
|
+
* `closed #N` / `gone`. */
|
|
1027
1033
|
evictedWhy?: string;
|
|
1034
|
+
/** What the tree held when that eviction removed it (item 17: never a
|
|
1035
|
+
* reason to keep the tree, never discarded silently): the tracked changes
|
|
1036
|
+
* and unpushed commits measured as the thread user. Absent when the tree
|
|
1037
|
+
* was clean, already gone with the disk, or not measured (a force detach). */
|
|
1038
|
+
evictedLeftBehind?: LeftBehind;
|
|
1039
|
+
/** Why the tree could not be measured before that eviction (git could not
|
|
1040
|
+
* read it) — so an unreadable tree is never recorded as clean. */
|
|
1041
|
+
evictedUnmeasured?: string;
|
|
1042
|
+
/** What the tree held when the last disk-full recycle (item 54) discarded
|
|
1043
|
+
* it with the disk — the same measurement as `evictedLeftBehind`, under its
|
|
1044
|
+
* own name because the binding is not evicted: its user is kept and the
|
|
1045
|
+
* next attach recreates the tree (`worktree-missing`). Both rewritten on
|
|
1046
|
+
* every recycle; absent when the tree was clean or already gone. */
|
|
1047
|
+
recycledLeftBehind?: LeftBehind;
|
|
1048
|
+
/** Why the tree could not be measured before that recycle (git could not
|
|
1049
|
+
* read it) — still discarded with the disk, never recorded as clean. */
|
|
1050
|
+
recycledUnmeasured?: string;
|
|
1028
1051
|
/** How deps were last materialized (evidence that the per-branch reconciliation ran). */
|
|
1029
1052
|
deps?: ThreadDepsMechanism;
|
|
1030
1053
|
/** Commit the worktree was last attached at (the ref's tip in the mirror
|
|
@@ -2933,12 +2956,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2933
2956
|
throw new CycleRestartError("image-stale-restart");
|
|
2934
2957
|
}
|
|
2935
2958
|
|
|
2936
|
-
// Idle sleep:
|
|
2937
|
-
//
|
|
2959
|
+
// Idle sleep: no live binding used within IDLE_AFTER_S and nothing in
|
|
2960
|
+
// flight → skip this fetch; the cron holds the next instance to the idle
|
|
2938
2961
|
// cadence, so SLEEP_AFTER can elapse. Staleness is repaid at the next
|
|
2939
|
-
// attach (refreshIfStale).
|
|
2940
|
-
//
|
|
2941
|
-
// uncommitted work is not snapshotted.
|
|
2962
|
+
// attach (refreshIfStale). What a live tree holds is not asked (item 17):
|
|
2963
|
+
// sleep destroys the disk, and a tree no run is using protects nothing.
|
|
2942
2964
|
// Only a SETTLED resident may park: a cycle that finds `refreshing`/
|
|
2943
2965
|
// `restoring` at entry is looking at a marker left by a cycle that died
|
|
2944
2966
|
// mid-flight (a deploy evicting the DO: stuck `refreshing` + parked →
|
|
@@ -2990,7 +3012,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2990
3012
|
await this.ctx.storage.delete(DEGRADED_STREAK_KEY);
|
|
2991
3013
|
}
|
|
2992
3014
|
if (settled && (await this.isIdle())) {
|
|
2993
|
-
// isIdle awaited (
|
|
3015
|
+
// isIdle awaited (the bindings listing) — re-read before writing.
|
|
2994
3016
|
const now = (await this.ctx.storage.get<RepoFacts>(FACTS_KEY)) ?? facts;
|
|
2995
3017
|
if (!now.idleSince)
|
|
2996
3018
|
await this.ctx.storage.put(FACTS_KEY, {
|
|
@@ -3786,15 +3808,25 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3786
3808
|
});
|
|
3787
3809
|
}
|
|
3788
3810
|
|
|
3789
|
-
/**
|
|
3790
|
-
*
|
|
3791
|
-
*
|
|
3792
|
-
|
|
3793
|
-
|
|
3811
|
+
/** Whether a run may be using this disk: a live binding attached to or
|
|
3812
|
+
* used (an exec bumps `lastAttachAt` too, item 21) within IDLE_AFTER_S.
|
|
3813
|
+
* The op counter is 0 between a run's tool calls, so this floor is what
|
|
3814
|
+
* stands for a run mid-flight. The one predicate behind "the disk may go
|
|
3815
|
+
* away": the idle-sleep gate (item 16b — a platform sleep destroys the
|
|
3816
|
+
* disk) and the disk-full recycle (item 54) both read it, and neither asks
|
|
3817
|
+
* what a tree holds (item 17): a dirty tree no run is using protects
|
|
3818
|
+
* nothing — the next attach wipes it. Bindings are storage, so this needs
|
|
3819
|
+
* no container. */
|
|
3820
|
+
private recentlyUsed(live: ThreadBinding[]): boolean {
|
|
3794
3821
|
const recent = systemClock() - IDLE_AFTER_S * 1000;
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3822
|
+
return live.some((b) => Date.parse(b.lastAttachAt) >= recent);
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
/** Idle = no live binding used within the floor and nothing in flight; the
|
|
3826
|
+
* sweep's release records what an idle tree held. */
|
|
3827
|
+
private async isIdle(): Promise<boolean> {
|
|
3828
|
+
if (this.recentlyUsed(await this.liveBindings())) return false;
|
|
3829
|
+
return this.inFlightCount() === 0;
|
|
3798
3830
|
}
|
|
3799
3831
|
|
|
3800
3832
|
/** Bindings that hold a pool user and a tree on disk (not evicted). */
|
|
@@ -3803,18 +3835,6 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3803
3835
|
return [...all.values()].filter((b) => !b.evicted && b.user);
|
|
3804
3836
|
}
|
|
3805
3837
|
|
|
3806
|
-
/** True when no live tree holds work a lost disk would destroy: every one
|
|
3807
|
-
* is clean (as its thread user), or the runtime is already down — a sleep
|
|
3808
|
-
* has destroyed the disk, so there is nothing left to lose. */
|
|
3809
|
-
private async liveTreesClean(live: ThreadBinding[]): Promise<boolean> {
|
|
3810
|
-
if (!(await this.isRuntimeActive().catch(() => false))) return true;
|
|
3811
|
-
// Concurrent: each check touches only its own (disjoint) tree, and one
|
|
3812
|
-
// spawn each — the idle gate does not pay a serial
|
|
3813
|
-
// 3-probe round-trip per live binding.
|
|
3814
|
-
const checks = await Promise.all(live.map((b) => this.worktreeCleanliness(b)));
|
|
3815
|
-
return checks.every((c) => c.clean); // any dirty or unknown → not clean
|
|
3816
|
-
}
|
|
3817
|
-
|
|
3818
3838
|
// -- disk-full (docs/reference/specs/resident-repos.md item 54) ---------------------------
|
|
3819
3839
|
|
|
3820
3840
|
/** Free space on the workspace mount in KiB; `null` when df cannot answer.
|
|
@@ -3840,28 +3860,54 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3840
3860
|
|
|
3841
3861
|
/** The disk is a cache: stop the container so the next cycle attempt
|
|
3842
3862
|
* restores mirror + checkout from R2 onto an empty disk — the same wake
|
|
3843
|
-
* path as a platform sleep. Only when the pure plan allows it:
|
|
3844
|
-
* flight (`selfInFlight` excludes the
|
|
3845
|
-
*
|
|
3846
|
-
*
|
|
3847
|
-
*
|
|
3848
|
-
*
|
|
3863
|
+
* path as a platform sleep. Only when the pure plan allows it: no recycle
|
|
3864
|
+
* within the cooldown, nothing in flight (`selfInFlight` excludes the
|
|
3865
|
+
* calling refresh cycle from the count) and no live binding used within
|
|
3866
|
+
* the idle floor — the idle-sleep gate's own predicate (`recentlyUsed`),
|
|
3867
|
+
* because a sleep and a recycle destroy the same disk: it may go away when
|
|
3868
|
+
* no run is using it, never for what the trees hold (item 17). A recycle
|
|
3869
|
+
* discards every live tree the way an eviction does, so each is measured
|
|
3870
|
+
* first, as its thread user, and its binding records what went
|
|
3871
|
+
* (`recordRecycledTree`); the plan is decided again after those awaits,
|
|
3872
|
+
* right before the stop. A refused recycle is written to
|
|
3873
|
+
* `lastRefreshError` with its why, so `/residents` says what an operator
|
|
3874
|
+
* must do; the `degraded` reason stays the clean `disk-full: …`. Answers
|
|
3875
|
+
* whether the container was recycled. */
|
|
3849
3876
|
private async recoverFromDiskFull(reason: string, selfInFlight: number): Promise<boolean> {
|
|
3850
3877
|
const lastRecycleAt = await this.ctx.storage.get<number>(DISK_FULL_RECYCLE_KEY);
|
|
3851
|
-
const plan =
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3878
|
+
const plan = (live: ThreadBinding[]) =>
|
|
3879
|
+
planDiskFullRecovery({
|
|
3880
|
+
now: systemClock(),
|
|
3881
|
+
lastRecycleAt,
|
|
3882
|
+
inFlight: this.inFlightCount() - selfInFlight,
|
|
3883
|
+
recentlyUsed: this.recentlyUsed(live),
|
|
3884
|
+
idleFloorS: IDLE_AFTER_S,
|
|
3885
|
+
});
|
|
3886
|
+
const kept = async (why: string) => {
|
|
3887
|
+
console.log(`disk-full: container kept — ${why}`);
|
|
3888
|
+
await this.recordRefreshError(`${reason} — container kept: ${why}`);
|
|
3860
3889
|
return false;
|
|
3861
|
-
}
|
|
3890
|
+
};
|
|
3891
|
+
const live = await this.liveBindings();
|
|
3892
|
+
const first = plan(live);
|
|
3893
|
+
if (first.action === "wait") return kept(first.why);
|
|
3894
|
+
// What each live tree holds, for its record — never a reason to keep the
|
|
3895
|
+
// container. Concurrent: each probe touches only its own tree, one spawn
|
|
3896
|
+
// each. Runtime down: the disk, and every tree with it, is already gone.
|
|
3897
|
+
const active = await this.isRuntimeActive().catch(() => false);
|
|
3898
|
+
const trees = active
|
|
3899
|
+
? await Promise.all(
|
|
3900
|
+
live.map(async (binding) => [binding, await this.measureTreeBeforeEviction(binding)] as const),
|
|
3901
|
+
)
|
|
3902
|
+
: live.map((binding) => [binding, undefined] as const);
|
|
3903
|
+
// The measurement awaited (the DO yields at each await): decide again over
|
|
3904
|
+
// fresh facts — an attach or an op that landed meanwhile keeps the container.
|
|
3905
|
+
const verdict = plan(await this.liveBindings());
|
|
3906
|
+
if (verdict.action === "wait") return kept(verdict.why);
|
|
3862
3907
|
console.log(
|
|
3863
3908
|
`disk-full: recycling the container — the next cycle restores mirror + checkout from R2 onto an empty disk (${reason})`,
|
|
3864
3909
|
);
|
|
3910
|
+
for (const [binding, tree] of trees) await this.recordRecycledTree(binding, tree);
|
|
3865
3911
|
await this.ctx.storage.put(DISK_FULL_RECYCLE_KEY, systemClock());
|
|
3866
3912
|
await this.recordRefreshError(`${reason} — container recycled; restoring from R2 on the next cycle`);
|
|
3867
3913
|
this.swapIncarnation(); // deliberate incarnation swap
|
|
@@ -3869,6 +3915,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3869
3915
|
return true;
|
|
3870
3916
|
}
|
|
3871
3917
|
|
|
3918
|
+
/** The disk-full recycle's record for one live tree (item 54), written the
|
|
3919
|
+
* way `evictBinding` writes an eviction's — but the binding stays live, its
|
|
3920
|
+
* pool user kept and its tree recreated on the next attach, so this is not
|
|
3921
|
+
* an eviction and the record sits under its own name beside
|
|
3922
|
+
* `evictedLeftBehind`: the counts when something was there, the probe's
|
|
3923
|
+
* first error line when git could not read the tree, both rewritten on
|
|
3924
|
+
* every recycle so nothing lingers from an earlier one, and one log line
|
|
3925
|
+
* when the tree held something. Re-read first: a binding evicted or
|
|
3926
|
+
* re-attached during the measurement is left as is. */
|
|
3927
|
+
private async recordRecycledTree(binding: ThreadBinding, tree: EvictedTree | undefined): Promise<void> {
|
|
3928
|
+
const now = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(binding.threadKey));
|
|
3929
|
+
if (!now || now.evicted || now.lastAttachAt !== binding.lastAttachAt) return;
|
|
3930
|
+
await this.ctx.storage.put(threadBindingKey(binding.threadKey), {
|
|
3931
|
+
...now,
|
|
3932
|
+
recycledLeftBehind: tree && "leftBehind" in tree ? tree.leftBehind : undefined,
|
|
3933
|
+
recycledUnmeasured: tree && "unmeasured" in tree ? tree.unmeasured : undefined,
|
|
3934
|
+
} satisfies ThreadBinding);
|
|
3935
|
+
if (tree)
|
|
3936
|
+
console.log(`disk-full: ${binding.threadKey} tree discarded with the disk — ${evictedTreeSentence(tree)}`);
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3872
3939
|
// -- disk budget (docs/reference/specs/resident-repos.md item 55) -------------------------
|
|
3873
3940
|
|
|
3874
3941
|
/** The `df` half: total/used/free of the workspace mount; null when df
|
|
@@ -3948,10 +4015,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3948
4015
|
* already on disk is `reuse` (0 — a dirty/stale recreate frees the old one
|
|
3949
4016
|
* first). A ref not yet in the mirror (fetched under the lock, moments later)
|
|
3950
4017
|
* is projected as `hardlink`, the common case. When it does not fit, the
|
|
3951
|
-
* coldest
|
|
4018
|
+
* coldest idle trees go first (`orderEvictionCandidates`: never the
|
|
3952
4019
|
* requesting thread, a busy tree, the default branch, or one attached within
|
|
3953
|
-
* DISK_EVICT_MIN_IDLE_MS
|
|
3954
|
-
*
|
|
4020
|
+
* DISK_EVICT_MIN_IDLE_MS), whatever they hold — item 17: dirt never keeps a
|
|
4021
|
+
* tree; each is measured as its thread user for the eviction's record —
|
|
4022
|
+
* `df` re-probed after each; still
|
|
3955
4023
|
* short → `503 {reason:"disk-pressure"}` with the whole math in `error`, the
|
|
3956
4024
|
* same shape as `mirror-busy`, so the bot falls back cold legibly. Never a
|
|
3957
4025
|
* lifecycle flip: the checkout is intact and every existing tree keeps
|
|
@@ -4021,12 +4089,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4021
4089
|
if (verdict.fits) break;
|
|
4022
4090
|
const binding = live.find((b) => b.threadKey === c.threadKey);
|
|
4023
4091
|
if (!binding) continue;
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
}
|
|
4029
|
-
// Same re-read guards as the sweep: the clean check awaited.
|
|
4092
|
+
// What the tree holds goes on the eviction's record (item 17), never
|
|
4093
|
+
// into a keep; the runtime is up — this attach is running on it.
|
|
4094
|
+
const tree = await this.measureTreeBeforeEviction(binding);
|
|
4095
|
+
// Same re-read guards as the sweep: the measurement awaited.
|
|
4030
4096
|
const current = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(c.threadKey));
|
|
4031
4097
|
if (
|
|
4032
4098
|
!current ||
|
|
@@ -4037,7 +4103,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4037
4103
|
kept.push({ threadKey: c.threadKey, why: "busy" });
|
|
4038
4104
|
continue;
|
|
4039
4105
|
}
|
|
4040
|
-
if (!(await this.evictBinding(current, true, "disk-pressure", DISK_PRESSURE_REASON))) {
|
|
4106
|
+
if (!(await this.evictBinding(current, true, "disk-pressure", DISK_PRESSURE_REASON, tree))) {
|
|
4041
4107
|
kept.push({ threadKey: c.threadKey, why: "other" });
|
|
4042
4108
|
continue;
|
|
4043
4109
|
}
|
|
@@ -6022,13 +6088,21 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6022
6088
|
|
|
6023
6089
|
/** Remove a thread's worktree (when the runtime is up — a slept container
|
|
6024
6090
|
* already lost it) and release its pool user; the binding is KEPT, marked
|
|
6025
|
-
* evicted
|
|
6026
|
-
*
|
|
6091
|
+
* evicted with the cause (`evictedWhy`) and with what the tree held
|
|
6092
|
+
* (`evictedLeftBehind`, or `evictedUnmeasured` when git could not read it
|
|
6093
|
+
* — item 17: never a reason to keep the tree, never discarded silently), so
|
|
6094
|
+
* the ref stays sticky and the next attach recreates the tree with it.
|
|
6095
|
+
* Shared by the inactivity sweep, the reclamation pass, the disk-pressure
|
|
6096
|
+
* path and /detach. */
|
|
6027
6097
|
private async evictBinding(
|
|
6028
6098
|
binding: ThreadBinding,
|
|
6029
6099
|
runtimeActive: boolean,
|
|
6030
6100
|
logCtx: string,
|
|
6031
6101
|
why: string,
|
|
6102
|
+
/** What the caller measured in the tree first (`measureTreeBeforeEviction`,
|
|
6103
|
+
* as the thread user with the runtime up); absent when the tree was clean,
|
|
6104
|
+
* already gone with the disk, or not measured (a force detach). */
|
|
6105
|
+
tree?: EvictedTree,
|
|
6032
6106
|
): Promise<boolean> {
|
|
6033
6107
|
const threadDir = parentDir(binding.worktreePath);
|
|
6034
6108
|
if (runtimeActive && threadDir.startsWith(`${THREADS_DIR}/`)) {
|
|
@@ -6064,7 +6138,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6064
6138
|
evicted: true,
|
|
6065
6139
|
evictedAt: new Date(systemClock()).toISOString(),
|
|
6066
6140
|
evictedWhy: why,
|
|
6141
|
+
evictedLeftBehind: tree && "leftBehind" in tree ? tree.leftBehind : undefined,
|
|
6142
|
+
evictedUnmeasured: tree && "unmeasured" in tree ? tree.unmeasured : undefined,
|
|
6067
6143
|
} satisfies ThreadBinding);
|
|
6144
|
+
if (tree) console.log(`${logCtx}: ${binding.threadKey} evicted (${why}) — ${evictedTreeSentence(tree)}`);
|
|
6068
6145
|
return true;
|
|
6069
6146
|
}
|
|
6070
6147
|
|
|
@@ -6105,17 +6182,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6105
6182
|
if (left > 0) return { released: false, reason: busyAfterKillReason(left), user: binding.user };
|
|
6106
6183
|
}
|
|
6107
6184
|
const active = await this.isRuntimeActive().catch(() => false);
|
|
6108
|
-
// What the tree still holds, for the answer
|
|
6109
|
-
// Not measured on a force release: a read-only
|
|
6110
|
-
// hard stop's tree is whatever the killed
|
|
6111
|
-
// fails names nothing
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
leftBehind = leftBehindOf(measured);
|
|
6116
|
-
if (!measured.clean && leftBehind === undefined)
|
|
6117
|
-
console.log(`detach: ${threadKey}: the tree could not be measured before its release (${measured.reason})`);
|
|
6118
|
-
}
|
|
6185
|
+
// What the tree still holds, for the answer and the eviction's record —
|
|
6186
|
+
// never a reason to keep it. Not measured on a force release: a read-only
|
|
6187
|
+
// tree holds nothing, and a hard stop's tree is whatever the killed
|
|
6188
|
+
// command left. A probe that fails names nothing in the answer (never a
|
|
6189
|
+
// guess); the record and the log say it could not be measured.
|
|
6190
|
+
let tree: EvictedTree | undefined;
|
|
6191
|
+
if (!force && active) tree = await this.measureTreeBeforeEviction(binding);
|
|
6119
6192
|
// Re-check right before removal: the measurement above awaited (the DO
|
|
6120
6193
|
// yields at each await), so an exec that arrived mid-detach would otherwise
|
|
6121
6194
|
// have its tree removed under it.
|
|
@@ -6136,16 +6209,12 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6136
6209
|
// Same as the sweep: `active` was read before the measurement's awaits; a
|
|
6137
6210
|
// container that woke meanwhile must get the rm, not an orphaned tree.
|
|
6138
6211
|
const activeNow = await this.isRuntimeActive().catch(() => false);
|
|
6139
|
-
if (!(await this.evictBinding(current, activeNow, `detach`, "detach"))) {
|
|
6212
|
+
if (!(await this.evictBinding(current, activeNow, `detach`, "detach", tree))) {
|
|
6140
6213
|
return { released: false, reason: "re-attached during eviction — kept", user };
|
|
6141
6214
|
}
|
|
6142
|
-
if (leftBehind) {
|
|
6143
|
-
console.log(
|
|
6144
|
-
`detach: ${threadKey} released — left behind ${leftBehind.uncommittedChanges} uncommitted change(s) and ${leftBehind.unpushedCommits} unpushed commit(s), discarded with the tree`,
|
|
6145
|
-
);
|
|
6146
|
-
}
|
|
6147
6215
|
// Item 55: the tree is gone; the gauge catches up at the next refresh
|
|
6148
6216
|
// instance's `measure` step, and the admission's `df` sees the space now.
|
|
6217
|
+
const leftBehind = tree && "leftBehind" in tree ? tree.leftBehind : undefined;
|
|
6149
6218
|
return { released: true, user, ...(leftBehind !== undefined ? { leftBehind } : {}) };
|
|
6150
6219
|
}
|
|
6151
6220
|
|
|
@@ -6183,20 +6252,21 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6183
6252
|
}
|
|
6184
6253
|
}
|
|
6185
6254
|
|
|
6186
|
-
/**
|
|
6187
|
-
*
|
|
6188
|
-
*
|
|
6189
|
-
*
|
|
6190
|
-
*
|
|
6191
|
-
*
|
|
6192
|
-
*
|
|
6193
|
-
*
|
|
6255
|
+
/** What this thread's tree holds: the tracked changes and unpushed commits,
|
|
6256
|
+
* or that git could not read it. The git probes run AS THE THREAD USER
|
|
6257
|
+
* (su), never as root: the worktree is thread-owned, so root git in it
|
|
6258
|
+
* would be refused by safe.directory and would be the exact repo-local-
|
|
6259
|
+
* config execution vector safe.directory exists to block. Read by
|
|
6260
|
+
* `measureTreeBeforeEviction` alone, for the record of an eviction or of
|
|
6261
|
+
* the disk-full recycle — never a reason to keep a tree or a container
|
|
6262
|
+
* (item 17). A tree that no longer exists (disk recycled by a sleep/wake)
|
|
6263
|
+
* is clean with nothing measured: there was nothing to discard.
|
|
6194
6264
|
*
|
|
6195
6265
|
* ONE spawn: the presence test and both git probes fold
|
|
6196
6266
|
* into the pure `worktreeCleanlinessScript` (test -d as root, both git
|
|
6197
6267
|
* commands inside a single privilege-dropped `su`, tagged lines out);
|
|
6198
6268
|
* `parseWorktreeCleanliness` encodes the exact decision table above. */
|
|
6199
|
-
private async worktreeCleanliness(binding: ThreadBinding): Promise<
|
|
6269
|
+
private async worktreeCleanliness(binding: ThreadBinding): Promise<WorktreeCleanliness> {
|
|
6200
6270
|
const injected = { GIT_TERMINAL_PROMPT: "0" }; // same injection as threadRun — fail fast, never prompt
|
|
6201
6271
|
validateEnvNames(injected);
|
|
6202
6272
|
const r = await this.run(["sh", "-c", worktreeCleanlinessScript(binding.worktreePath, binding.user)], {
|
|
@@ -6206,6 +6276,15 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6206
6276
|
return parseWorktreeCleanliness(r);
|
|
6207
6277
|
}
|
|
6208
6278
|
|
|
6279
|
+
/** What a tree holds right before its eviction — or before the disk-full
|
|
6280
|
+
* recycle discards it with the disk (item 54) — for the record and the log
|
|
6281
|
+
* (item 17): the one-spawn probe as the thread user, read once into
|
|
6282
|
+
* `evictedTreeOf` — never into a keep decision. Callers skip it when the
|
|
6283
|
+
* runtime is down: the disk, and the tree with it, is already gone. */
|
|
6284
|
+
private async measureTreeBeforeEviction(binding: ThreadBinding): Promise<EvictedTree | undefined> {
|
|
6285
|
+
return evictedTreeOf(await this.worktreeCleanliness(binding));
|
|
6286
|
+
}
|
|
6287
|
+
|
|
6209
6288
|
/** Anything that must not be interrupted by a container stop or counted
|
|
6210
6289
|
* as idle: thread exec/read/write, disposable /op runs, and attaches past
|
|
6211
6290
|
* their own image check (mid clone/install under the mirror lock). */
|
|
@@ -6278,8 +6357,17 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6278
6357
|
// attach), and an eviction decided on a stale "inactive" would skip the
|
|
6279
6358
|
// rm and orphan a real tree.
|
|
6280
6359
|
const activeNow = await this.isRuntimeActive().catch(() => false);
|
|
6360
|
+
// What the tree holds goes on the eviction's record (item 17), measured
|
|
6361
|
+
// while the runtime is up; a slept container has no tree to measure.
|
|
6362
|
+
const tree = activeNow ? await this.measureTreeBeforeEviction(current) : undefined;
|
|
6281
6363
|
if (
|
|
6282
|
-
await this.evictBinding(
|
|
6364
|
+
await this.evictBinding(
|
|
6365
|
+
current,
|
|
6366
|
+
activeNow,
|
|
6367
|
+
`worktree-sweep ${resource}`,
|
|
6368
|
+
last >= cutoff ? "clean-idle" : "ttl",
|
|
6369
|
+
tree,
|
|
6370
|
+
)
|
|
6283
6371
|
)
|
|
6284
6372
|
evicted.push(binding.threadKey);
|
|
6285
6373
|
else kept++;
|
|
@@ -6537,9 +6625,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6537
6625
|
* mirror (the refresh cycle's `fetch --prune` just ran) or its PR was
|
|
6538
6626
|
* merged/closed. Runs inside the refresh cycle — a poll on the existing
|
|
6539
6627
|
* cadence, since the GitHub App has webhooks off — and via /debug
|
|
6540
|
-
* reclaim-now. Never touches the default branch
|
|
6541
|
-
*
|
|
6542
|
-
*
|
|
6628
|
+
* reclaim-now. Never touches the default branch or a busy thread
|
|
6629
|
+
* (reclaimDecision); every keep is named. What the tree holds is never a
|
|
6630
|
+
* reason to keep it (item 17) — it is measured once as the thread user and
|
|
6631
|
+
* recorded by the eviction, the sweep's `evictBinding` with the same
|
|
6632
|
+
* re-read guards. */
|
|
6543
6633
|
async reclaimFinishedRefs(
|
|
6544
6634
|
resource: string,
|
|
6545
6635
|
defaultRef: string,
|
|
@@ -6599,21 +6689,16 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6599
6689
|
// by name), so its fate is never looked up.
|
|
6600
6690
|
const { fate, detail } = (!isDefaultRef && fates.get(binding.ref)) || { fate: "unknown" as RefFate, detail: "" };
|
|
6601
6691
|
const busy = this.threadOpsInFlight.get(binding.threadKey) ?? 0;
|
|
6602
|
-
|
|
6603
|
-
// anything: a finished ref, nothing running, runtime up (down → the tree
|
|
6604
|
-
// is already gone with the disk → null).
|
|
6605
|
-
const finished = fate === "gone" || fate === "merged" || fate === "closed";
|
|
6606
|
-
const clean = !active
|
|
6607
|
-
? null
|
|
6608
|
-
: finished && !isDefaultRef && busy === 0
|
|
6609
|
-
? (await this.worktreeCleanliness(binding)).clean
|
|
6610
|
-
: null;
|
|
6611
|
-
const decision = reclaimDecision({ fate, isDefaultRef, busy, clean });
|
|
6692
|
+
const decision = reclaimDecision({ fate, isDefaultRef, busy });
|
|
6612
6693
|
if (!decision.reclaim) {
|
|
6613
6694
|
kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: decision.why });
|
|
6614
6695
|
continue;
|
|
6615
6696
|
}
|
|
6616
|
-
//
|
|
6697
|
+
// What the tree holds goes on the eviction's record (item 17), never
|
|
6698
|
+
// into the decision: measured as the thread user while the runtime is
|
|
6699
|
+
// up; down → the tree is already gone with the disk.
|
|
6700
|
+
const tree = active ? await this.measureTreeBeforeEviction(binding) : undefined;
|
|
6701
|
+
// Same guards as the sweep: the measurement awaited, so re-read the
|
|
6617
6702
|
// binding (a re-attach means a fresh tree) and the op counter.
|
|
6618
6703
|
const busyNow = this.threadOpsInFlight.get(binding.threadKey) ?? 0;
|
|
6619
6704
|
const current = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(binding.threadKey));
|
|
@@ -6627,7 +6712,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6627
6712
|
}
|
|
6628
6713
|
const activeNow = await this.isRuntimeActive().catch(() => false);
|
|
6629
6714
|
const why = `${decision.why}${detail}`;
|
|
6630
|
-
if (await this.evictBinding(current, activeNow, `reclaim ${resource}`, why)) {
|
|
6715
|
+
if (await this.evictBinding(current, activeNow, `reclaim ${resource}`, why, tree)) {
|
|
6631
6716
|
reclaimed.push({ threadKey: binding.threadKey, ref: binding.ref, why });
|
|
6632
6717
|
console.log(`reclaim ${resource}: evicted ${binding.threadKey} on ${binding.ref} — ${why}`);
|
|
6633
6718
|
} else kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: "re-attached" });
|
|
@@ -6738,18 +6823,39 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6738
6823
|
// left out; nothing here is secret (credential files are never persisted).
|
|
6739
6824
|
const threads = [...bindings.values()]
|
|
6740
6825
|
.sort((a, b) => b.lastAttachAt.localeCompare(a.lastAttachAt))
|
|
6741
|
-
.map(
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6826
|
+
.map(
|
|
6827
|
+
({
|
|
6828
|
+
threadKey,
|
|
6829
|
+
ref,
|
|
6830
|
+
sha,
|
|
6831
|
+
user,
|
|
6832
|
+
deps,
|
|
6833
|
+
boundAt,
|
|
6834
|
+
lastAttachAt,
|
|
6835
|
+
evicted,
|
|
6836
|
+
evictedAt,
|
|
6837
|
+
evictedWhy,
|
|
6838
|
+
evictedLeftBehind,
|
|
6839
|
+
evictedUnmeasured,
|
|
6840
|
+
recycledLeftBehind,
|
|
6841
|
+
recycledUnmeasured,
|
|
6842
|
+
}) => ({
|
|
6843
|
+
threadKey,
|
|
6844
|
+
ref,
|
|
6845
|
+
sha: sha ?? null,
|
|
6846
|
+
user,
|
|
6847
|
+
deps: deps ?? null,
|
|
6848
|
+
boundAt,
|
|
6849
|
+
lastAttachAt,
|
|
6850
|
+
evicted: evicted ?? false,
|
|
6851
|
+
evictedAt: evictedAt ?? null,
|
|
6852
|
+
evictedWhy: evictedWhy ?? null,
|
|
6853
|
+
evictedLeftBehind: evictedLeftBehind ?? null,
|
|
6854
|
+
evictedUnmeasured: evictedUnmeasured ?? null,
|
|
6855
|
+
recycledLeftBehind: recycledLeftBehind ?? null,
|
|
6856
|
+
recycledUnmeasured: recycledUnmeasured ?? null,
|
|
6857
|
+
}),
|
|
6858
|
+
);
|
|
6753
6859
|
return {
|
|
6754
6860
|
resource: map.get(RESOURCE_KEY) ?? null,
|
|
6755
6861
|
state: map.get(STATE_KEY) ?? "down",
|
|
@@ -176,6 +176,17 @@ RUN npm install -g @earendil-works/pi-coding-agent@0.85.1 \
|
|
|
176
176
|
&& pi --version | grep -qx '0.85.1' \
|
|
177
177
|
&& pi --help | grep -q -- '--mode <mode>'
|
|
178
178
|
|
|
179
|
+
# OpenCode, the second harness (docs/reference/specs/harness.md): `@opencode/cli`
|
|
180
|
+
# at the EXACT pin held by src/deploy/imageOpenCodeHarness.test.ts, beside pi.
|
|
181
|
+
# The wrapper's postinstall hard-links the platform binary into place, so it
|
|
182
|
+
# is the one install script allowed by name; the grep proves the binary on
|
|
183
|
+
# PATH prints the pin's version text (`opencode v2.0.3`, exactly); the npm
|
|
184
|
+
# cache (a 200 MB tarball) is dropped in the same layer. Dark until a
|
|
185
|
+
# deployment names it: nothing here starts it on its own.
|
|
186
|
+
RUN npm install -g --allow-scripts=@opencode/cli @opencode/cli@2.0.3 \
|
|
187
|
+
&& npm cache clean --force \
|
|
188
|
+
&& opencode --version | grep -qx 'opencode v2.0.3'
|
|
189
|
+
|
|
179
190
|
# The container outlives a crash of the SDK's container server (the
|
|
180
191
|
# supervisor's header says why). The base image's ENTRYPOINT is tini running
|
|
181
192
|
# the server, so one uncaught exception there ended the container; here tini
|