@coreplane/switchboard 1.241.0 → 1.243.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 (157) hide show
  1. package/dist/assets/config/config.example.yaml +45 -13
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +86 -29
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +298 -24
  4. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +38 -5
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +1 -1
  7. package/dist/assets/project.json +5 -5
  8. package/dist/assets/source.json +3 -3
  9. package/dist/assets/src/agents/registry.ts +61 -48
  10. package/dist/assets/src/config/profile.ts +68 -3
  11. package/dist/assets/src/core/authz/actor.ts +12 -9
  12. package/dist/assets/src/core/authz/authorize.ts +17 -4
  13. package/dist/assets/src/core/authz/policy.ts +4 -0
  14. package/dist/assets/src/core/authz/types.ts +12 -0
  15. package/dist/assets/src/core/budgets.ts +313 -0
  16. package/dist/assets/src/core/coordinator/contract.ts +3 -1
  17. package/dist/assets/src/core/coordinator/driver.ts +0 -10
  18. package/dist/assets/src/core/costs.ts +21 -80
  19. package/dist/assets/src/core/harness/scope.ts +21 -0
  20. package/dist/assets/src/core/modelPricing.ts +212 -0
  21. package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
  22. package/dist/assets/src/core/reviewVerdict.ts +7 -0
  23. package/dist/assets/src/core/runEvents.ts +48 -5
  24. package/dist/assets/src/core/runFriction.ts +2 -1
  25. package/dist/assets/src/core/runLedger/types.ts +2 -0
  26. package/dist/assets/src/core/runRecord.ts +51 -0
  27. package/dist/assets/src/core/runUsage.ts +158 -47
  28. package/dist/assets/src/core/schedules.ts +3 -0
  29. package/dist/assets/src/core/ship/contract.ts +4 -2
  30. package/dist/assets/src/core/ship/coordinator.ts +101 -84
  31. package/dist/assets/src/core/ship/handoff.ts +9 -0
  32. package/dist/assets/src/execution/bashTimeout.ts +8 -5
  33. package/dist/assets/src/execution/residentAutoRebuild.ts +4 -2
  34. package/dist/assets/src/execution/residentHead.ts +23 -10
  35. package/dist/assets/src/execution/residentInfraStreak.ts +89 -0
  36. package/dist/assets/src/execution/residentRefresh.ts +43 -4
  37. package/dist/assets/src/execution/residentSteps.ts +1 -0
  38. package/dist/assets/src/execution/sandboxErrors.ts +6 -0
  39. package/dist/assets/src/execution/seedPlan.ts +17 -0
  40. package/dist/assets/web/dist/.vite/manifest.json +443 -446
  41. package/dist/assets/web/dist/assets/{AppShell-DEy5jRuS.js → AppShell-IDMGG6yi.js} +1 -1
  42. package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +2 -0
  43. package/dist/assets/web/dist/assets/{DeliveryPage-BGpPr5xr.js → DeliveryPage-n1tz5I_v.js} +1 -1
  44. package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +2 -0
  45. package/dist/assets/web/dist/assets/{NotFoundPage-DtmQM2gD.js → NotFoundPage-C97EhJcQ.js} +1 -1
  46. package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +1 -0
  47. package/dist/assets/web/dist/assets/{ResidentDetailPage-C-48fxat.js → ResidentDetailPage-DACalNVF.js} +1 -1
  48. package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +1 -0
  49. package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +1 -0
  50. package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +9 -0
  51. package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +1 -0
  52. package/dist/assets/web/dist/assets/{RunsTabs-CtNGNnhN.js → RunsTabs-BUfdk0lH.js} +1 -1
  53. package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +1 -0
  54. package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +1 -0
  55. package/dist/assets/web/dist/assets/{StatusDot-DPWE1JmO.js → StatusDot-Dw0T1M-P.js} +1 -1
  56. package/dist/assets/web/dist/assets/{Tooltip-DKhSRH9t.js → Tooltip-BbLuIAiS.js} +1 -1
  57. package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +1 -0
  58. package/dist/assets/web/dist/assets/{angular-html-DgSK1qvr.js → angular-html-oBNfPJR0.js} +1 -1
  59. package/dist/assets/web/dist/assets/{angular-ts-D3gNdiSG.js → angular-ts-BvNwsyWA.js} +1 -1
  60. package/dist/assets/web/dist/assets/{apl-CkHCYM8I.js → apl-CNUdRlYf.js} +1 -1
  61. package/dist/assets/web/dist/assets/{astro-DLm45axt.js → astro-Zb0NriSe.js} +1 -1
  62. package/dist/assets/web/dist/assets/{blade-BKa-VE-c.js → blade-qPRVheqq.js} +1 -1
  63. package/dist/assets/web/dist/assets/{c-C8NCNWai.js → c-D8Awx4YO.js} +1 -1
  64. package/dist/assets/web/dist/assets/{chapel-DqDQ7IKH.js → chapel-Bt72Mhsx.js} +1 -1
  65. package/dist/assets/web/dist/assets/{cobol-j4vCD6AJ.js → cobol-BOBacexg.js} +1 -1
  66. package/dist/assets/web/dist/assets/{coffee-C6oDVH-0.js → coffee-E4u0liHW.js} +1 -1
  67. package/dist/assets/web/dist/assets/{cpp-Bd3A1Rtc.js → cpp-q2sLNlul.js} +1 -1
  68. package/dist/assets/web/dist/assets/{crystal-CUOgOw0d.js → crystal-DyWqUnlb.js} +1 -1
  69. package/dist/assets/web/dist/assets/{css-RYljyv7G.js → css-CQY0hFsD.js} +1 -1
  70. package/dist/assets/web/dist/assets/{dist-BnJXKHCY.js → dist-twkFmSUY.js} +2 -2
  71. package/dist/assets/web/dist/assets/durationTone-BobbycC-.js +1 -0
  72. package/dist/assets/web/dist/assets/{edge-BJhNdQDc.js → edge-C1MwhJkX.js} +1 -1
  73. package/dist/assets/web/dist/assets/{elixir-g1AWOAfT.js → elixir-Bb3YbHfn.js} +1 -1
  74. package/dist/assets/web/dist/assets/{elm-CER5e4Dz.js → elm-DAN9IGQw.js} +1 -1
  75. package/dist/assets/web/dist/assets/{erb-D5aOmMQf.js → erb-BEB8Xlsj.js} +1 -1
  76. package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
  77. package/dist/assets/web/dist/assets/{git-rebase-D6rfV8jp.js → git-rebase-tqpjRfxO.js} +1 -1
  78. package/dist/assets/web/dist/assets/{glimmer-js-BvDH3-mC.js → glimmer-js-Ccbo65zR.js} +1 -1
  79. package/dist/assets/web/dist/assets/{glimmer-ts-DChtNs9V.js → glimmer-ts-B6WBVMpE.js} +1 -1
  80. package/dist/assets/web/dist/assets/{glsl-BHlsWrxf.js → glsl-tRec3Fcu.js} +1 -1
  81. package/dist/assets/web/dist/assets/{graphql-BsaBHb53.js → graphql-P8kbxT4F.js} +1 -1
  82. package/dist/assets/web/dist/assets/{hack-0lbDosCd.js → hack-H9Zhkagy.js} +1 -1
  83. package/dist/assets/web/dist/assets/{haml-BKtco2vr.js → haml-DtEnpn7Z.js} +1 -1
  84. package/dist/assets/web/dist/assets/{handlebars-BE_cj04z.js → handlebars-DkgPfoAz.js} +1 -1
  85. package/dist/assets/web/dist/assets/{html-BNcr8EVd.js → html-D30RXpIs.js} +1 -1
  86. package/dist/assets/web/dist/assets/{html-derivative-BsJt2Kei.js → html-derivative-DwozLrEx.js} +1 -1
  87. package/dist/assets/web/dist/assets/{http-BR5P8ER_.js → http-DXuzBAPm.js} +1 -1
  88. package/dist/assets/web/dist/assets/{hurl-CtHalpWS.js → hurl-d1UUJIt_.js} +1 -1
  89. package/dist/assets/web/dist/assets/indexRow-B_s5tKyq.js +1 -0
  90. package/dist/assets/web/dist/assets/{java-BXVdhN11.js → java-DL0gWf34.js} +1 -1
  91. package/dist/assets/web/dist/assets/{javascript-CyoShgNQ.js → javascript-Cl7vavnS.js} +1 -1
  92. package/dist/assets/web/dist/assets/{jinja-BQYZzHex.js → jinja-CfQOWMX9.js} +1 -1
  93. package/dist/assets/web/dist/assets/{jison-DqrUa1Eq.js → jison-KdYirlqm.js} +1 -1
  94. package/dist/assets/web/dist/assets/{json-CQiTF9Jj.js → json-C9cDQ-Qj.js} +1 -1
  95. package/dist/assets/web/dist/assets/{jsx-Y16MkCYQ.js → jsx-CRx5NItd.js} +1 -1
  96. package/dist/assets/web/dist/assets/{julia-zn16YONe.js → julia-CmsQsZQl.js} +1 -1
  97. package/dist/assets/web/dist/assets/{just-DcVeN_MN.js → just-CsM3Q8TE.js} +1 -1
  98. package/dist/assets/web/dist/assets/{latex-BNNoF-WP.js → latex-BXCh5YRX.js} +1 -1
  99. package/dist/assets/web/dist/assets/{liquid-DAi6qHzn.js → liquid-BF2vwK8p.js} +1 -1
  100. package/dist/assets/web/dist/assets/{lua-BswS0axw.js → lua-DcMBATrl.js} +1 -1
  101. package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +1 -0
  102. package/dist/assets/web/dist/assets/{main-B3is4sk6.js → main-d-w-tIKt.js} +2 -2
  103. package/dist/assets/web/dist/assets/{marko-C-4hkmQZ.js → marko-88MndKvG.js} +1 -1
  104. package/dist/assets/web/dist/assets/{mdc-0ZaHUkS7.js → mdc-DJ4kVAd8.js} +1 -1
  105. package/dist/assets/web/dist/assets/{nginx-DBDg5tOR.js → nginx-CP6mRgtV.js} +1 -1
  106. package/dist/assets/web/dist/assets/{nim-BVMz49Yw.js → nim-QQfj3fpF.js} +1 -1
  107. package/dist/assets/web/dist/assets/{org-1hO_K6ya.js → org-Bke3eBzc.js} +1 -1
  108. package/dist/assets/web/dist/assets/{perl-CVRQLvVk.js → perl-Bkh0N7Kz.js} +1 -1
  109. package/dist/assets/web/dist/assets/{php-K9nCrB9n.js → php-Dnv1Piya.js} +1 -1
  110. package/dist/assets/web/dist/assets/{pug-CTxOcmO3.js → pug-D6peFFI7.js} +1 -1
  111. package/dist/assets/web/dist/assets/{qml-CmCkfG5q.js → qml-D8PEs-C-.js} +1 -1
  112. package/dist/assets/web/dist/assets/{r-pP47Xn1X.js → r-B1EL9b_j.js} +1 -1
  113. package/dist/assets/web/dist/assets/{razor-BpW6r3tC.js → razor-BsMTIh2b.js} +1 -1
  114. package/dist/assets/web/dist/assets/{regexp-CINgdY4N.js → regexp-HhvC8spD.js} +1 -1
  115. package/dist/assets/web/dist/assets/{rst-BZIUEq2Q.js → rst-D5paAxpg.js} +1 -1
  116. package/dist/assets/web/dist/assets/{ruby-nzAOxz6r.js → ruby-BrQwhLrl.js} +1 -1
  117. package/dist/assets/web/dist/assets/{sas-QIS1bFth.js → sas-afot2B1o.js} +1 -1
  118. package/dist/assets/web/dist/assets/{scss-BuhBBVNt.js → scss-Efm-mwuG.js} +1 -1
  119. package/dist/assets/web/dist/assets/{shellscript-CQc1vXbk.js → shellscript-BK0Vv5fT.js} +1 -1
  120. package/dist/assets/web/dist/assets/{shellsession-CoubCAUv.js → shellsession-BGqlMC7N.js} +1 -1
  121. package/dist/assets/web/dist/assets/{soy-CPRzlder.js → soy-M0b4UwGM.js} +1 -1
  122. package/dist/assets/web/dist/assets/{sql-W9krb8-9.js → sql-sxe6IE9j.js} +1 -1
  123. package/dist/assets/web/dist/assets/sseReplay-C9m_EB8J.js +9 -0
  124. package/dist/assets/web/dist/assets/{stata-CuISJEC0.js → stata-nPF_ddLP.js} +1 -1
  125. package/dist/assets/web/dist/assets/{surrealql-CSet7584.js → surrealql-D_GrC6u7.js} +1 -1
  126. package/dist/assets/web/dist/assets/{svelte-ChXTSYwK.js → svelte-DwL1AtNP.js} +1 -1
  127. package/dist/assets/web/dist/assets/{templ-Bm55v62k.js → templ-7s7LTDkc.js} +1 -1
  128. package/dist/assets/web/dist/assets/{tex-CICMX8Gj.js → tex-Bx-5fMxe.js} +1 -1
  129. package/dist/assets/web/dist/assets/{ts-tags-CA1UzWyB.js → ts-tags-DUMJnke_.js} +1 -1
  130. package/dist/assets/web/dist/assets/{tsx-Dy04HNbv.js → tsx-BN8biPDe.js} +1 -1
  131. package/dist/assets/web/dist/assets/{twig-BBHsVnVD.js → twig-8nIu84TN.js} +1 -1
  132. package/dist/assets/web/dist/assets/{typescript-Dvc-wVBT.js → typescript-BooSPq_S.js} +1 -1
  133. package/dist/assets/web/dist/assets/{typst-Dvlqx3_q.js → typst-CTBiBsem.js} +1 -1
  134. package/dist/assets/web/dist/assets/{vue-zWi1MNU2.js → vue-C6Ft4Lea.js} +1 -1
  135. package/dist/assets/web/dist/assets/{vue-html-D4YxT3An.js → vue-html-Ba36dD5D.js} +1 -1
  136. package/dist/assets/web/dist/assets/{vue-vine-0YE7uQlH.js → vue-vine-NkFexVo2.js} +1 -1
  137. package/dist/assets/web/dist/assets/{xml-gm-iksZB.js → xml-C_THnHXZ.js} +1 -1
  138. package/dist/assets/web/dist/assets/{xsl-D_W5BcrY.js → xsl-D8G5xqjY.js} +1 -1
  139. package/dist/assets/web/dist/assets/{yaml-CHmZ21wZ.js → yaml-jAMIJzge.js} +1 -1
  140. package/dist/cli.js +4121 -1902
  141. package/package.json +1 -1
  142. package/dist/assets/web/dist/assets/CostsPage-BuHD-Bv7.js +0 -2
  143. package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +0 -2
  144. package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +0 -1
  145. package/dist/assets/web/dist/assets/ResidentsIndexPage-CBBIjPOa.js +0 -1
  146. package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +0 -1
  147. package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +0 -6
  148. package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +0 -1
  149. package/dist/assets/web/dist/assets/ScheduledPage-Ci1Ygqnc.js +0 -1
  150. package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +0 -1
  151. package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +0 -1
  152. package/dist/assets/web/dist/assets/UnitRoutePage-DEN5bSQf.js +0 -1
  153. package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +0 -1
  154. package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
  155. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
  156. package/dist/assets/web/dist/assets/main-DP_zSemY.css +0 -1
  157. package/dist/assets/web/dist/assets/sseReplay-yji8a1wM.js +0 -9
@@ -196,6 +196,15 @@ function entryLines(h: Handoff): Array<{ list: ListKey; bullet: string; row: str
196
196
  ];
197
197
  }
198
198
 
199
+ /** Each entry as one line, list by list — a deviation as `Deviation: from → to
200
+ * — why`, a follow-up as `what — where`, an unproven criterion as `Unproven:
201
+ * criterion — why` — the same words the ledger rows carry, for a reader that
202
+ * wants the handoff as plain lines (the thread's artifacts block). Empty for
203
+ * an empty handoff. */
204
+ export function handoffLines(h: Handoff): string[] {
205
+ return entryLines(h).map((e) => e.row);
206
+ }
207
+
199
208
  function prLink(pr: HandoffRenderContext["pr"]): string | undefined {
200
209
  return pr ? `[#${pr.number}](${pr.url})` : undefined;
201
210
  }
@@ -2,28 +2,31 @@
2
2
  // one set of bounds, shared by the tool layer, every executor, and both deploy
3
3
  // Workers (which import from src/execution like shellQuote/residentDetach).
4
4
  // Deliberately free of node: imports so wrangler can bundle it into Workers.
5
+ // The numbers are rows of src/core/budgets.ts (docs/decisions/0046); this file
6
+ // keeps their names for the readers that learned them here.
7
+ import { ALLOWANCES, BASH_COMMAND, MINUTE_MS } from "../core/budgets.js";
5
8
 
6
9
  /** Default per-command budget when the caller passes no timeoutMs. */
7
- export const BASH_TIMEOUT_MS = 5 * 60_000;
10
+ export const BASH_TIMEOUT_MS = BASH_COMMAND.defaultMinutes * MINUTE_MS;
8
11
 
9
12
  /** Floor: anything lower is a typo or an attack, not a budget. */
10
- export const BASH_TIMEOUT_MIN_MS = 1_000;
13
+ export const BASH_TIMEOUT_MIN_MS = BASH_COMMAND.minMs;
11
14
 
12
15
  /** Hard ceiling a caller can raise the budget to. Not by itself a guarantee
13
16
  * against one command eating a run — 20 minutes is 80% of a 25-minute review
14
17
  * — which is what `bashBudgetWithinRun` below is for. */
15
- export const BASH_TIMEOUT_MAX_MS = 20 * 60_000;
18
+ export const BASH_TIMEOUT_MAX_MS = BASH_COMMAND.maxMinutes * MINUTE_MS;
16
19
 
17
20
  /** Wall clock a run keeps back from its last command for the write-up: the
18
21
  * runner's deadline forces a final answer, and a command still running at
19
22
  * that moment would have been wasted anyway. */
20
- export const RUN_DEADLINE_RESERVE_MS = 60_000;
23
+ export const RUN_DEADLINE_RESERVE_MS = ALLOWANCES.commandWriteUp * MINUTE_MS;
21
24
 
22
25
  /** Margin the remote exec clients add to their HTTP wait over the command
23
26
  * budget, so the server's own timeout answer (a streamed exit 124) wins the
24
27
  * race against the client's transport deadline instead of both firing at the
25
28
  * same instant. */
26
- export const EXEC_CALL_MARGIN_MS = 30_000;
29
+ export const EXEC_CALL_MARGIN_MS = ALLOWANCES.execCall * MINUTE_MS;
27
30
 
28
31
  /** The documented clamp rule, applied identically bot-side and server-side
29
32
  * (a server never trusts the client's number): a finite number is truncated
@@ -9,8 +9,10 @@
9
9
  * cycle, and these say the snapshot (or the container that would restore it)
10
10
  * cannot be used — never a provision failure, which would loop against the
11
11
  * same broken build. `runtime-unreachable` is item 64's last rung: a recreated
12
- * container that did not answer either. */
13
- export const REHYDRATION_FAILURE_RE = /^(r2-restore-failed|snapshot-stamp-mismatch|no-snapshot|runtime-unreachable)/;
12
+ * container that did not answer either; `infra-streak` is item 67's: a
13
+ * recreated container whose cycles kept failing in the resident's own steps. */
14
+ export const REHYDRATION_FAILURE_RE =
15
+ /^(r2-restore-failed|snapshot-stamp-mismatch|no-snapshot|runtime-unreachable|infra-streak)/;
14
16
 
15
17
  /** How many auto-rebuilds one resident gets inside one window. A rebuild is a
16
18
  * clone, an install and a build (minutes to half an hour) that holds the cap
@@ -67,10 +67,9 @@ export type FetchReason = "missing-ref" | "stale-tip" | "returnable-ref";
67
67
  * lost and a ref a person named that the thread never pushed has no way
68
68
  * back, so neither pays this fetch: the refresh cycle is their freshness.
69
69
  * - otherwise null.
70
- * A fetch that STILL leaves the tip elsewhere (a push racing this attach, or
71
- * a force-push) is not this function's concern: the attach proceeds on the
72
- * fetched tip and reports it, and the reviewed-head guard decides what a
73
- * review of it may do. */
70
+ * A fetch that STILL leaves the tip elsewhere (a push racing this attach, a
71
+ * force-push, or a fetch that failed) is `attachTarget`'s concern: the attach
72
+ * is refused `stale-tip` rather than run at a commit nobody asked for. */
74
73
  export function mirrorFetchReason(input: {
75
74
  refExists: boolean;
76
75
  mirrorSha?: string;
@@ -85,8 +84,16 @@ export function mirrorFetchReason(input: {
85
84
 
86
85
  /** What the attach checks out, once the mirror is as fresh as it will get. */
87
86
  export type AttachTarget =
88
- /** The bound ref is in the mirror: clone its tip, as always. */
87
+ /** The bound ref is in the mirror, and its tip is the commit the caller
88
+ * named (or the caller named none): clone its tip, as always. */
89
89
  | { kind: "ref" }
90
+ /** The bound ref is in the mirror but its tip is NOT the commit the caller
91
+ * named, even after the fetch — the mirror is behind it (the fetch failed
92
+ * or was skipped) or ahead of it (a push raced the attach). Refused: a run
93
+ * executes at the sha it asked for, or not on this resident; the caller
94
+ * falls back cold at the requested commit. `tip` is null when the tip
95
+ * could not be read — never assumed fresh. */
96
+ | { kind: "stale-tip"; tip: string | null; want: string }
90
97
  /** The ref is gone but the commit the caller expects is in the mirror — a
91
98
  * merged PR's branch was deleted while `refs/pull/N/head` (a `--mirror`
92
99
  * clone carries every ref) still holds its head: check that commit out,
@@ -96,16 +103,22 @@ export type AttachTarget =
96
103
  /** Neither: the attach is refused as `unknown-ref`. */
97
104
  | { kind: "unknown-ref" };
98
105
 
99
- /** The attach target after the fetch (item 51). The ref wins whenever it
100
- * exists — a detached tree is only for a ref that is gone; a caller that
101
- * named no commit, or whose commit the mirror does not hold either, gets the
102
- * refusal it always got. */
106
+ /** The attach target after the fetch (item 51). A ref that exists is cloned
107
+ * at its tip when the caller named no commit or the tip is that commit;
108
+ * a tip that is any other commit is `stale-tip`, refused. A detached tree is
109
+ * only for a ref that is gone; a caller that named no commit, or whose commit
110
+ * the mirror does not hold either, gets the refusal it always got. */
103
111
  export function attachTarget(input: {
104
112
  refExists: boolean;
105
113
  wantSha: string | null;
106
114
  commitInMirror: boolean;
115
+ /** The ref's tip in the mirror after the fetch; null or absent when it could not be read. */
116
+ tipSha?: string | null;
107
117
  }): AttachTarget {
108
- if (input.refExists) return { kind: "ref" };
118
+ if (input.refExists) {
119
+ if (input.wantSha === null || input.tipSha === input.wantSha) return { kind: "ref" };
120
+ return { kind: "stale-tip", tip: input.tipSha ?? null, want: input.wantSha };
121
+ }
109
122
  if (input.wantSha !== null && input.commitInMirror) return { kind: "sha", sha: input.wantSha };
110
123
  return { kind: "unknown-ref" };
111
124
  }
@@ -0,0 +1,89 @@
1
+ // A refresh cycle that keeps failing in the resident's OWN steps heals itself
2
+ // (docs/reference/specs/resident-repos.md item 67). Pure: the Worker's
3
+ // `refreshFailed` bumps the row and acts on the rung; the cycle's gate asks
4
+ // `parksOnRepeat` before it counts a degraded reason toward the park streak.
5
+ //
6
+ // The split this module draws: a step that runs the repository's own command
7
+ // (the onboard-time command table — install, build, test) failing is evidence
8
+ // about the repository, and the right response is to park and wait for the
9
+ // head to be fixed, which is what the park streak does. Every other step is
10
+ // the resident's machinery — git against the mirror, the probes, the markers,
11
+ // the restores — and a failure there that repeats is the container or the
12
+ // disk gone wrong, which a rebuild does fix. Item 64 already climbs this
13
+ // ladder for one signature (the control port that never answers); this is
14
+ // the same ladder for every other resident-step failure.
15
+
16
+ import { STALE_SWEEP_SUFFIX } from "./residentSteps.js";
17
+
18
+ /** The steps that run the repository's own commands. */
19
+ export const REPO_COMMAND_STEPS: ReadonlySet<string> = new Set(["deps-install", "build", "test"]);
20
+
21
+ /** Whether a failed step ran the repository's command (evidence about the
22
+ * repo) rather than the resident's own machinery. The stale-process sweep
23
+ * that precedes a repo step is ours. */
24
+ export function isRepoCommandStep(step: string): boolean {
25
+ if (step.endsWith(STALE_SWEEP_SUFFIX)) return false;
26
+ return REPO_COMMAND_STEPS.has(step);
27
+ }
28
+
29
+ /** The step named by a `<step>-failed:` reason (`classifyRefreshFailure`'s
30
+ * plain-failure shape), or null for every other reason. */
31
+ export function stepOfFailedReason(reason: string): string | null {
32
+ const m = /^([a-z][a-z0-9-]*)-failed:/.exec(reason);
33
+ return m ? m[1] : null;
34
+ }
35
+
36
+ /** Whether a repeated degraded reason should PARK the resident (the 6-hour
37
+ * cadence, `DEGRADED_PARK_AFTER_CYCLES`): only evidence about the repository
38
+ * or about GitHub — a repo-command step's failure, or a reason that names no
39
+ * step at all (`github-unreachable`). A resident-step failure never parks:
40
+ * the ladder below is its escalation, and parking would only slow it. */
41
+ export function parksOnRepeat(reason: string): boolean {
42
+ const step = stepOfFailedReason(reason);
43
+ return step === null || isRepoCommandStep(step);
44
+ }
45
+
46
+ /** The ladder's rungs: consecutive cycles failed in resident steps. */
47
+ export const INFRA_STREAK_RECREATE_AT = 3;
48
+ export const INFRA_STREAK_DOWN_AT = 5;
49
+
50
+ export type InfraStreakRung = "count" | "recreate" | "down";
51
+
52
+ /** Count 1–2: record and let the next cycle try. Count 3: destroy the
53
+ * container, snapshots kept — the next cycle restores from the snapshot onto
54
+ * a fresh disk (item 64's rung 3). Count 4: the recreated container's one
55
+ * cycle of its own. Count ≥ 5: a fresh container failed the same way; `down`
56
+ * with a rehydration-flavored reason, so item 36's transition rebuild is the
57
+ * exit. */
58
+ export function infraStreakRung(count: number): InfraStreakRung {
59
+ if (!Number.isFinite(count) || count < 1) {
60
+ throw new RangeError(`infra-streak rung needs a count of at least 1, got ${count}`);
61
+ }
62
+ if (count >= INFRA_STREAK_DOWN_AT) return "down";
63
+ if (count === INFRA_STREAK_RECREATE_AT) return "recreate";
64
+ return "count";
65
+ }
66
+
67
+ /** The persisted row (`resident:infraStreak`): the last failing step, the
68
+ * consecutive count and the span. */
69
+ export interface InfraStreakRow {
70
+ step: string;
71
+ count: number;
72
+ firstAt: string;
73
+ lastAt: string;
74
+ }
75
+
76
+ const INFRA_STREAK_ACTION: Readonly<Record<Exclude<InfraStreakRung, "count">, string>> = {
77
+ recreate: "the container was destroyed, snapshots kept; the next cycle restores from the snapshot",
78
+ down: "a recreated container failed the same way; rebuilding",
79
+ };
80
+
81
+ /** The reason a rung records: `infra-streak:` so the gate, the serviceability
82
+ * check and item 36's eligibility all read it as the resident's, never the repo's. */
83
+ export function infraStreakReason(row: InfraStreakRow, rung: Exclude<InfraStreakRung, "count">): string {
84
+ return `infra-streak: ${row.count} consecutive cycles failed in the resident's own steps (last: ${row.step}, since ${row.firstAt}) — ${INFRA_STREAK_ACTION[rung]}`;
85
+ }
86
+
87
+ export function isInfraStreakReason(reason: string): boolean {
88
+ return /^infra-streak: /.test(reason);
89
+ }
@@ -132,6 +132,10 @@ export interface RefreshFailure {
132
132
  * with the attempt count (the ladder below decides what the resident does
133
133
  * about it); real failures keep `<step>-failed:`. */
134
134
  reason: string;
135
+ /** The step that failed, as the caller named it (`refresh` for a failure
136
+ * between steps): item 67 reads whether it ran the repository's own command
137
+ * or the resident's machinery. */
138
+ step: string;
135
139
  interrupted: boolean;
136
140
  diskFull: boolean;
137
141
  runtimeUnreachable: boolean;
@@ -205,9 +209,34 @@ const INTERRUPTION_SIGNATURE = /\bexit 143\b|Session terminated|SIGTERM|^restore
205
209
  * exec path and the refresh classifier agree on one wording list (a container
206
210
  * stop mid-snapshot produces "Process supervisor is closed"; classified as the
207
211
  * repo's own `snapshot-failed` it would end the instance `degraded` instead of
208
- * being retried into the same step). */
209
- export const RUNTIME_REPLACEMENT_WORDING =
210
- /previous runtime incarnation|interrupted because the runtime changed|runtime identity is no longer active|sandbox lifetime is no longer current|platform was updating the sandbox runtime|no longer identifies pid|process supervisor is closed|container is not running, consider calling start/i;
212
+ * being retried into the same step).
213
+ *
214
+ * The list is the union of two a reader tells apart, because the resident's
215
+ * `/exec` answer treats them differently (resident-repos item 43): the SDK's
216
+ * words that vouch the runtime MOVED — a new incarnation serves the thread,
217
+ * so the container the command was aimed at is gone whatever else is known
218
+ * — and the platform's words for a container that is merely DOWN, which say
219
+ * nothing about a replacement: a stopped container answers "Process
220
+ * supervisor is closed" to a spawn, and the workerd binding answers "The
221
+ * container is not running" whether the container is asleep, starting, or
222
+ * gone under a roll. For the refresh classifier and `isRuntimeReplacement`
223
+ * the union is right: a step interrupted either way is retried onto the
224
+ * container that comes back. */
225
+ export const RUNTIME_MOVED_WORDING =
226
+ /previous runtime incarnation|interrupted because the runtime changed|runtime identity is no longer active|sandbox lifetime is no longer current|platform was updating the sandbox runtime|no longer identifies pid/i;
227
+ /** The platform's words for a container that is down under a command (see
228
+ * `RUNTIME_MOVED_WORDING`): the spawn refusal of a stopped container and the
229
+ * binding's not-running refusal, anchored to the full phrase so a genuine
230
+ * crash ("container exited with unexpected exit code") and the readiness
231
+ * probe ("the container is not listening") never match. The harness's
232
+ * container seam reads the same two as a container down under its one more
233
+ * command — a wait, never a judgement (harness.md item 6). */
234
+ export const STOPPED_CONTAINER_WORDING =
235
+ /process supervisor is closed|container is not running, consider calling start/i;
236
+ export const RUNTIME_REPLACEMENT_WORDING = new RegExp(
237
+ `${RUNTIME_MOVED_WORDING.source}|${STOPPED_CONTAINER_WORDING.source}`,
238
+ "i",
239
+ );
211
240
 
212
241
  /** `runtimeUnreachable` is the caller's word that the failure was the SDK's
213
242
  * connect abort at the exec choke point (the Worker's typed check over the
@@ -234,6 +263,7 @@ export function classifyRefreshFailure(input: {
234
263
  const { step, message } = input;
235
264
  if (input.runtimeUnreachable) {
236
265
  return {
266
+ step,
237
267
  interrupted: false,
238
268
  diskFull: false,
239
269
  runtimeUnreachable: true,
@@ -242,6 +272,7 @@ export function classifyRefreshFailure(input: {
242
272
  }
243
273
  if (isDiskFullMessage(message)) {
244
274
  return {
275
+ step,
245
276
  interrupted: false,
246
277
  diskFull: true,
247
278
  runtimeUnreachable: false,
@@ -251,6 +282,7 @@ export function classifyRefreshFailure(input: {
251
282
  const timedOut = /\(timed out\)/.test(message);
252
283
  if (!timedOut && (INTERRUPTION_SIGNATURE.test(message) || RUNTIME_REPLACEMENT_WORDING.test(message))) {
253
284
  return {
285
+ step,
254
286
  interrupted: true,
255
287
  diskFull: false,
256
288
  runtimeUnreachable: false,
@@ -259,13 +291,20 @@ export function classifyRefreshFailure(input: {
259
291
  }
260
292
  if (input.freeKiB !== undefined && input.freeKiB !== null && input.freeKiB < DISK_FULL_FREE_KIB) {
261
293
  return {
294
+ step,
262
295
  interrupted: false,
263
296
  diskFull: true,
264
297
  runtimeUnreachable: false,
265
298
  reason: diskFullReason({ step, message, freeKiB: input.freeKiB }),
266
299
  };
267
300
  }
268
- return { interrupted: false, diskFull: false, runtimeUnreachable: false, reason: `${step}-failed: ${message}` };
301
+ return {
302
+ step,
303
+ interrupted: false,
304
+ diskFull: false,
305
+ runtimeUnreachable: false,
306
+ reason: `${step}-failed: ${message}`,
307
+ };
269
308
  }
270
309
 
271
310
  // -- the runtime that never answers --------------------------------------------
@@ -49,6 +49,7 @@ export const RESIDENT_STEP_LABELS = {
49
49
  "worktree-clean": "cleaning the worktree",
50
50
  "clean-workspace": "cleaning the workspace",
51
51
  "clean-before-restore": "cleaning before the restore",
52
+ "break-mirror": "removing the mirror's config (fault injection)",
52
53
  "unmount-restores": "unmounting earlier restores",
53
54
  "mirror-restore-extract": "extracting the mirror snapshot",
54
55
  "checkout-restore-extract": "extracting the checkout snapshot",
@@ -53,6 +53,12 @@ const FLEET_BUSY_PATTERNS: readonly RegExp[] = [
53
53
  // pool matches on this exact phrase. Seen live passing through as a plain
54
54
  // in-body error and ending two reviews in under a minute each.
55
55
  /Maximum number of running container instances exceeded/i,
56
+ // The platform's start-rate limit: a burst of fresh threads (twenty-four
57
+ // seeded sandboxes started within twenty seconds) had a third of its
58
+ // container starts refused with this text, and the Worker read it as a
59
+ // failed seed. It is capacity for a moment, like a full fleet: the
60
+ // executor waits and re-sends the identical request.
61
+ /too many containers per second/i,
56
62
  ];
57
63
 
58
64
  export function isFleetBusy(message: string): boolean {
@@ -70,6 +70,19 @@ export interface SandboxSeed {
70
70
 
71
71
  export type SeedStep = "restore" | "deps" | "fixup";
72
72
 
73
+ /** One restore's two phases, in milliseconds: the SDK's presigned download of
74
+ * the archive (judged by bytes arriving), then the extraction onto the disk. */
75
+ export interface RestorePhases {
76
+ download: number;
77
+ extract: number;
78
+ }
79
+
80
+ export interface SeedPhases {
81
+ checkout: RestorePhases;
82
+ /** Null when no deps entry rode along. */
83
+ deps: RestorePhases | null;
84
+ }
85
+
73
86
  export type SeedAnswer =
74
87
  | {
75
88
  seeded: true;
@@ -82,6 +95,10 @@ export type SeedAnswer =
82
95
  from: { ref: string; sha: string; checkoutBackupId: string; depsBackupId?: string };
83
96
  /** Milliseconds per step; `deps` null when no entry rode along; all zero when cached. */
84
97
  steps: { restore: number; deps: number | null; fixup: number };
98
+ /** Each restore split into its two phases — the archive's download to the
99
+ * container and its extraction onto the disk — so the gate's reading says
100
+ * which one owns a slow seed. Absent when cached. */
101
+ phases?: SeedPhases;
85
102
  ms: number;
86
103
  }
87
104
  | { seeded: false; reason: SeedReason; detail: string; step?: SeedStep };