@coreplane/switchboard 1.251.0 → 1.253.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.
Files changed (167) hide show
  1. package/dist/assets/config/config.example.yaml +28 -2
  2. package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
  3. package/dist/assets/deploy/cloudflare/worker.ts +6 -3
  4. package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +223 -38
  6. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
  7. package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
  8. package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
  9. package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
  10. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
  11. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
  12. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
  13. package/dist/assets/deploy/profile.example.json +1 -1
  14. package/dist/assets/package-lock.json +3 -3
  15. package/dist/assets/package.json +3 -2
  16. package/dist/assets/project.json +13 -9
  17. package/dist/assets/source.json +3 -3
  18. package/dist/assets/src/agents/registry.ts +5 -5
  19. package/dist/assets/src/core/authz/policy.ts +8 -0
  20. package/dist/assets/src/core/budgets.ts +39 -0
  21. package/dist/assets/src/core/coordinator/contract.ts +93 -1
  22. package/dist/assets/src/core/coordinator/driver.ts +236 -21
  23. package/dist/assets/src/core/drain.ts +50 -0
  24. package/dist/assets/src/core/memory/engine.ts +123 -4
  25. package/dist/assets/src/core/memory/scorer.ts +12 -4
  26. package/dist/assets/src/core/memory/types.ts +73 -12
  27. package/dist/assets/src/core/modelCard.ts +32 -4
  28. package/dist/assets/src/core/modelPricing.ts +111 -1
  29. package/dist/assets/src/core/modelProxy/usage.ts +88 -0
  30. package/dist/assets/src/core/modelRegistry.ts +15 -1
  31. package/dist/assets/src/core/refusal.ts +4 -7
  32. package/dist/assets/src/core/reviewVerdict.ts +4 -0
  33. package/dist/assets/src/core/runEvents.ts +82 -3
  34. package/dist/assets/src/core/runFriction.ts +8 -2
  35. package/dist/assets/src/core/runLedger/types.ts +11 -0
  36. package/dist/assets/src/core/runMetrics.ts +237 -0
  37. package/dist/assets/src/core/runRecord.ts +46 -0
  38. package/dist/assets/src/core/runUsage.ts +67 -13
  39. package/dist/assets/src/core/schedules.ts +3 -0
  40. package/dist/assets/src/core/ship/contract.ts +34 -14
  41. package/dist/assets/src/core/ship/coordinator.ts +485 -55
  42. package/dist/assets/src/core/ship/renewal.ts +10 -5
  43. package/dist/assets/src/core/trace/attrs.ts +24 -0
  44. package/dist/assets/src/core/types.ts +5 -5
  45. package/dist/assets/src/core/verbosity.ts +48 -0
  46. package/dist/assets/src/deploy/liveGate.ts +40 -13
  47. package/dist/assets/src/deploy/profile.ts +16 -0
  48. package/dist/assets/src/deploy/restart.ts +11 -12
  49. package/dist/assets/src/execution/residentDepCache.ts +50 -1
  50. package/dist/assets/src/execution/residentDepsStore.ts +40 -2
  51. package/dist/assets/src/execution/residentRefresh.ts +55 -3
  52. package/dist/assets/src/execution/residentSteps.ts +4 -0
  53. package/dist/assets/src/execution/sandboxErrors.ts +8 -0
  54. package/dist/assets/web/dist/.vite/manifest.json +444 -430
  55. package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
  56. package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
  57. package/dist/assets/web/dist/assets/{DeliveryPage-DUXd-Sl-.js → DeliveryPage-C7jaEJip.js} +1 -1
  58. package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
  59. package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
  60. package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
  61. package/dist/assets/web/dist/assets/{PendingTurnRow-DDhMhrI7.js → PendingTurnRow-C-pUgyyX.js} +1 -1
  62. package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
  63. package/dist/assets/web/dist/assets/{ResidentDetailPage-BnEoOnGQ.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
  64. package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
  65. package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
  66. package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
  67. package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
  68. package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
  69. package/dist/assets/web/dist/assets/{ScheduledPage-B_GgeJrb.js → ScheduledPage-C8FYoZy5.js} +1 -1
  70. package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
  71. package/dist/assets/web/dist/assets/{SettingsPage-BXX4R113.js → SettingsPage-CkYIhLUl.js} +1 -1
  72. package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-C0TUgnC1.js} +1 -1
  73. package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-CN5cZdl3.js} +1 -1
  74. package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.js → UnitRoutePage-D_EtAl8r.js} +1 -1
  75. package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
  76. package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
  77. package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
  78. package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
  79. package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
  80. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
  81. package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
  82. package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
  83. package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
  84. package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
  85. package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
  86. package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
  87. package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
  88. package/dist/assets/web/dist/assets/{dist-BCVXeBJ9.js → dist-B1Mf2JbM.js} +2 -2
  89. package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
  90. package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
  91. package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
  92. package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
  93. package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
  94. package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
  95. package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
  96. package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
  97. package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
  98. package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
  99. package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
  100. package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
  101. package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
  102. package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
  103. package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
  104. package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
  105. package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
  106. package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
  107. package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
  108. package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
  109. package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
  110. package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
  111. package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
  112. package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
  113. package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
  114. package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
  115. package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
  116. package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
  117. package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
  118. package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
  119. package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
  120. package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
  121. package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
  122. package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
  123. package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
  124. package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
  125. package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
  126. package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
  127. package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
  128. package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
  129. package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
  130. package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
  131. package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
  132. package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
  133. package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
  134. package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
  135. package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
  136. package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
  137. package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
  138. package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
  139. package/dist/assets/web/dist/assets/sseReplay-BYo2EZKy.js +11 -0
  140. package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
  141. package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
  142. package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
  143. package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
  144. package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
  145. package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
  146. package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
  147. package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
  148. package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
  149. package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
  150. package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
  151. package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
  152. package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
  153. package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
  154. package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
  155. package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
  156. package/dist/cli.js +5623 -2455
  157. package/package.json +1 -1
  158. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
  159. package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
  160. package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
  161. package/dist/assets/web/dist/assets/RunRoutePage-9klVWhSF.js +0 -9
  162. package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
  163. package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
  164. package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
  165. package/dist/assets/web/dist/assets/main-Dkcbtu3u.js +0 -28
  166. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
  167. package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +0 -9
@@ -0,0 +1,212 @@
1
+ // The resident guards its own memory (docs/reference/specs/resident-repos.md
2
+ // item 70) — the PURE half, kept free of the Sandbox SDK and DO storage so it
3
+ // runs under plain-Node vitest (memoryGuard.test.ts) like gc.ts and drain.ts.
4
+ // The Worker owns the one real reader (a single exec of CGROUP_READ_ARGV via
5
+ // its exec choke point) and feeds this module the raw output; tests feed a
6
+ // fake. A container at its cgroup memory cap wedges its own control port and
7
+ // every run on it loses its work, so the resident refuses NEW work by name
8
+ // while it still can: above the soft threshold a new attach is refused like
9
+ // `mirror-busy` (the bot falls back cold or waits, the card says why), above
10
+ // the hard threshold a new exec is refused with the numbers — and a command
11
+ // already running is never touched: the gate sits at each route's start and
12
+ // kills nothing.
13
+ import { MEMORY_PRESSURE_REASON } from "../../src/execution/sandboxErrors.js";
14
+
15
+ export { MEMORY_PRESSURE_REASON };
16
+
17
+ /** Above this percent of the cgroup memory cap a NEW `/attach` is refused —
18
+ * new runs go elsewhere while the ones already here finish. */
19
+ export const MEMORY_SOFT_LIMIT_PCT = 80;
20
+
21
+ /** Above this percent a NEW `/exec` is refused too — the resident answers
22
+ * nothing but the work already in flight, which always runs to completion. */
23
+ export const MEMORY_HARD_LIMIT_PCT = 90;
24
+
25
+ /** One reading of the container's cgroup v2 accounting. */
26
+ export interface MemoryReading {
27
+ /** ISO timestamp the caller supplied (the resident's clock seam). */
28
+ at: string;
29
+ /** memory.current — bytes charged to the cgroup now. */
30
+ usedBytes: number;
31
+ /** memory.max — the cap in bytes, or null when the file reads `max` (no cap). */
32
+ capBytes: number | null;
33
+ /** used/cap rounded to whole percent; null without a cap (nothing to gate on). */
34
+ percent: number | null;
35
+ /** cpu.stat's usage_usec, for the same log line; null when absent. */
36
+ cpuUsageUsec: number | null;
37
+ }
38
+
39
+ /** The reader seam: one call returns the raw output of `CGROUP_READ_ARGV`.
40
+ * A throw of `MemorySampleUnavailable` means "no reading this time" (the
41
+ * container busy or replaced under the probe) and never disables the gate;
42
+ * any other throw is an unreadable cgroup and disables it (one log line). */
43
+ export interface CgroupReader {
44
+ read(): Promise<string>;
45
+ }
46
+
47
+ /** The one transient escape: the reader could not run at all right now.
48
+ * `invalidates` says the container the last reading came from is gone (a
49
+ * runtime replacement under the probe), so the guard forgets that reading —
50
+ * a dead container's numbers must not gate the work that replaces it. */
51
+ export class MemorySampleUnavailable extends Error {
52
+ constructor(
53
+ message: string,
54
+ readonly invalidates = false,
55
+ ) {
56
+ super(message);
57
+ }
58
+ }
59
+
60
+ /** The separator `CGROUP_READ_ARGV` prints between the three files. */
61
+ export const CGROUP_FILE_SEPARATOR = ":::";
62
+
63
+ /** One exec, three files: memory.current, memory.max, cpu.stat from the
64
+ * container's own cgroup v2 root. `&&` so a missing file is a non-zero exit
65
+ * (an unreadable cgroup), never a silently short output. */
66
+ export const CGROUP_READ_ARGV: readonly string[] = [
67
+ "sh",
68
+ "-c",
69
+ `cat /sys/fs/cgroup/memory.current && echo ${CGROUP_FILE_SEPARATOR} && ` +
70
+ `cat /sys/fs/cgroup/memory.max && echo ${CGROUP_FILE_SEPARATOR} && cat /sys/fs/cgroup/cpu.stat`,
71
+ ];
72
+
73
+ /** Parses one reader output into a reading; throws on anything malformed. */
74
+ export function parseCgroupOutput(raw: string, at: string): MemoryReading {
75
+ const parts = raw.split(CGROUP_FILE_SEPARATOR).map((p) => p.trim());
76
+ if (parts.length !== 3) throw new Error(`expected 3 cgroup sections, got ${parts.length}`);
77
+ const usedBytes = Number(parts[0]);
78
+ if (!Number.isFinite(usedBytes) || usedBytes < 0) throw new Error(`memory.current is not a byte count: ${parts[0]}`);
79
+ let capBytes: number | null = null;
80
+ if (parts[1] !== "max") {
81
+ capBytes = Number(parts[1]);
82
+ if (!Number.isFinite(capBytes) || capBytes <= 0) throw new Error(`memory.max is not a byte count: ${parts[1]}`);
83
+ }
84
+ const usage = /(?:^|\n)usage_usec (\d+)/.exec(parts[2]);
85
+ const cpuUsageUsec = usage ? Number(usage[1]) : null;
86
+ const percent = capBytes === null ? null : Math.round((usedBytes / capBytes) * 100);
87
+ return { at, usedBytes, capBytes, percent, cpuUsageUsec };
88
+ }
89
+
90
+ /** The one structured line Workers Logs gets per sample: used bytes, cap
91
+ * bytes, percent (and the cpu counter beside them). */
92
+ export function memoryLogLine(r: MemoryReading): string {
93
+ const cap = r.capBytes === null ? "uncapped" : `${r.capBytes} bytes cap`;
94
+ const pct = r.percent === null ? "" : ` (${r.percent}%)`;
95
+ const cpu = r.cpuUsageUsec === null ? "" : ` — cpu usage_usec ${r.cpuUsageUsec}`;
96
+ return `memory: used ${r.usedBytes} bytes of ${cap}${pct}${cpu}`;
97
+ }
98
+
99
+ /** Which route asks the gate. */
100
+ export type MemoryGateRoute = "attach" | "exec";
101
+
102
+ /** A refusal with the numbers on it — the message is the whole story. */
103
+ export interface MemoryRefusal {
104
+ reason: typeof MEMORY_PRESSURE_REASON;
105
+ percent: number;
106
+ usedBytes: number;
107
+ capBytes: number;
108
+ message: string;
109
+ }
110
+
111
+ /** The pure gate over one reading: below both thresholds nothing changes; at
112
+ * or above the soft one a new attach is refused (queued — the caller waits or
113
+ * falls back, like `mirror-busy`); at or above the hard one a new exec is
114
+ * refused too. No reading, or no cap, gates nothing. */
115
+ export function gateMemory(route: MemoryGateRoute, reading: MemoryReading | null): MemoryRefusal | null {
116
+ if (!reading || reading.percent === null || reading.capBytes === null) return null;
117
+ const { percent, usedBytes, capBytes } = reading;
118
+ const refusal = (message: string): MemoryRefusal => ({
119
+ reason: MEMORY_PRESSURE_REASON,
120
+ percent,
121
+ usedBytes,
122
+ capBytes,
123
+ message,
124
+ });
125
+ if (route === "exec" && percent >= MEMORY_HARD_LIMIT_PCT) {
126
+ return refusal(
127
+ `${MEMORY_PRESSURE_REASON}: resident at ${percent}% of its memory cap (${usedBytes} of ${capBytes} bytes) — ` +
128
+ `a new command is refused above ${MEMORY_HARD_LIMIT_PCT}% while the commands already running finish`,
129
+ );
130
+ }
131
+ if (route === "attach" && percent >= MEMORY_SOFT_LIMIT_PCT) {
132
+ return refusal(
133
+ `${MEMORY_PRESSURE_REASON}: resident near its memory cap, ${percent}% used (${usedBytes} of ${capBytes} bytes), ` +
134
+ `queued — a new attach is refused above ${MEMORY_SOFT_LIMIT_PCT}% until the runs here release memory`,
135
+ );
136
+ }
137
+ return null;
138
+ }
139
+
140
+ /** The message of whatever was thrown (local: this module stays dependency-free). */
141
+ const causeText = (err: unknown): string => (err instanceof Error ? err.message : String(err));
142
+
143
+ /** The guard one resident holds: samples through the reader, keeps the last
144
+ * reading, gates the two routes — and on an unreadable cgroup logs ONCE and
145
+ * disables itself for the incarnation instead of refusing everything. */
146
+ export class MemoryGuard {
147
+ private disabledWhy: string | null = null;
148
+ private last: MemoryReading | null = null;
149
+
150
+ constructor(
151
+ private readonly reader: CgroupReader,
152
+ private readonly log: (line: string) => void,
153
+ ) {}
154
+
155
+ /** The last reading taken this incarnation, for `/status` and `/residents`. */
156
+ get lastReading(): MemoryReading | null {
157
+ return this.last;
158
+ }
159
+
160
+ /** Whether an unreadable cgroup turned the gate off (the why is logged once). */
161
+ get disabled(): boolean {
162
+ return this.disabledWhy !== null;
163
+ }
164
+
165
+ /** Forgets the last reading: the container it measured no longer runs (it
166
+ * went inactive, or was replaced under the probe), so nothing gates on it
167
+ * until a fresh sample lands. The gate's disabled state is untouched. */
168
+ invalidate(): void {
169
+ this.last = null;
170
+ }
171
+
172
+ /** One reader call, one log line. A `MemorySampleUnavailable` keeps the last
173
+ * reading and the gate as they were — unless it `invalidates` (the runtime
174
+ * was replaced under the probe), which forgets the reading so a stale one
175
+ * never masks the route's own runtime-replaced answer; any other failure
176
+ * disables the gate. */
177
+ async sample(at: string): Promise<MemoryReading | null> {
178
+ if (this.disabledWhy !== null) return null;
179
+ let raw: string;
180
+ try {
181
+ raw = await this.reader.read();
182
+ } catch (err) {
183
+ if (err instanceof MemorySampleUnavailable) {
184
+ if (err.invalidates) this.last = null;
185
+ return null;
186
+ }
187
+ this.disable(causeText(err));
188
+ return null;
189
+ }
190
+ let reading: MemoryReading;
191
+ try {
192
+ reading = parseCgroupOutput(raw, at);
193
+ } catch (err) {
194
+ this.disable(causeText(err));
195
+ return null;
196
+ }
197
+ this.last = reading;
198
+ this.log(memoryLogLine(reading));
199
+ return reading;
200
+ }
201
+
202
+ /** The route's verdict over the last reading; a disabled gate refuses nothing. */
203
+ gate(route: MemoryGateRoute): MemoryRefusal | null {
204
+ if (this.disabledWhy !== null) return null;
205
+ return gateMemory(route, this.last);
206
+ }
207
+
208
+ private disable(why: string): void {
209
+ this.disabledWhy = why;
210
+ this.log(`memory: cgroup unreadable — the memory gate is disabled for this incarnation (${why})`);
211
+ }
212
+ }
@@ -58,7 +58,7 @@ export interface RefreshInstanceAction {
58
58
  * (`stepTimeoutMs`), so a step timeout and a command timeout agree. */
59
59
  const REFRESH_FETCH_STEP_BUDGET_MS = RESTORE_MAX_MS + GIT_NETWORK_TIMEOUT_MS; // a wake's restore, then the fetch
60
60
  const REFRESH_INSTALL_STEP_BUDGET_MS = REFRESH_INSTALL_TIMEOUT_MS + DEPS_STEP_OVERHEAD_MS; // the install's own lease
61
- const REFRESH_BUILD_STEP_BUDGET_MS = GIT_NETWORK_TIMEOUT_MS + REFRESH_BUILD_TIMEOUT_MS; // the build's mutex lease
61
+ const REFRESH_BUILD_STEP_BUDGET_MS = 5 * GIT_NETWORK_TIMEOUT_MS + 2 * REFRESH_BUILD_TIMEOUT_MS; // five git-budgeted commands — stage-clear, stage, the staging fetch (under the stage lock), the checkout update and the swap (under the swap lock) — plus the off-lock reset/clean, deps re-link and build
62
62
  const REFRESH_SNAPSHOT_STEP_BUDGET_MS = R2_TRANSFER_TIMEOUT_MS + GIT_NETWORK_TIMEOUT_MS; // the archives, then the reclaim pass
63
63
  /** The sweep: one cleanliness check per binding idle past an hour (at most the
64
64
  * pool's worth, one exec budget each), then the removals under the mirror lock. */