@coreplane/switchboard 1.242.0 → 1.244.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 (153) hide show
  1. package/dist/assets/config/config.example.yaml +20 -1
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +234 -29
  3. package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
  4. package/dist/assets/deploy/cloudflare-resident/worker.ts +483 -75
  5. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
  6. package/dist/assets/package-lock.json +3 -3
  7. package/dist/assets/package.json +1 -1
  8. package/dist/assets/project.json +3 -3
  9. package/dist/assets/source.json +3 -3
  10. package/dist/assets/src/agents/registry.ts +31 -35
  11. package/dist/assets/src/config/profile.ts +68 -3
  12. package/dist/assets/src/core/authz/policy.ts +4 -0
  13. package/dist/assets/src/core/authz/types.ts +10 -0
  14. package/dist/assets/src/core/budgets.ts +349 -0
  15. package/dist/assets/src/core/coordinator/contract.ts +32 -0
  16. package/dist/assets/src/core/coordinator/driver.ts +75 -22
  17. package/dist/assets/src/core/costs.ts +9 -76
  18. package/dist/assets/src/core/delivery.ts +1 -1
  19. package/dist/assets/src/core/modelPricing.ts +212 -0
  20. package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
  21. package/dist/assets/src/core/reviewVerdict.ts +7 -0
  22. package/dist/assets/src/core/runEvents.ts +72 -5
  23. package/dist/assets/src/core/runFriction.ts +3 -1
  24. package/dist/assets/src/core/runRecord.ts +78 -0
  25. package/dist/assets/src/core/runUsage.ts +158 -47
  26. package/dist/assets/src/core/schedules.ts +3 -0
  27. package/dist/assets/src/core/ship/contract.ts +2 -1
  28. package/dist/assets/src/core/ship/coordinator.ts +312 -97
  29. package/dist/assets/src/core/ship/handoff.ts +9 -0
  30. package/dist/assets/src/core/ship/renewal.ts +155 -0
  31. package/dist/assets/src/core/trace/attrs.ts +11 -0
  32. package/dist/assets/src/core/trace/types.ts +4 -3
  33. package/dist/assets/src/execution/bashTimeout.ts +8 -5
  34. package/dist/assets/src/execution/residentRefresh.ts +52 -7
  35. package/dist/assets/src/execution/residentSteps.ts +1 -1
  36. package/dist/assets/src/execution/residentText.ts +8 -3
  37. package/dist/assets/src/mcp/registry.ts +3 -0
  38. package/dist/assets/web/dist/.vite/manifest.json +432 -429
  39. package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
  40. package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
  41. package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
  42. package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
  43. package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
  44. package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +1 -0
  45. package/dist/assets/web/dist/assets/{ResidentDetailPage-DTMBgnIW.js → ResidentDetailPage-C5USV_8P.js} +1 -1
  46. package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +1 -0
  47. package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
  48. package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
  49. package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
  50. package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
  51. package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +1 -0
  52. package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
  53. package/dist/assets/web/dist/assets/{StatusDot-ELoXHlFt.js → StatusDot-_Q_bmReh.js} +1 -1
  54. package/dist/assets/web/dist/assets/{Tooltip-BoeFwYP2.js → Tooltip-Cafsgd4m.js} +1 -1
  55. package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +1 -0
  56. package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
  57. package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
  58. package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
  59. package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
  60. package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
  61. package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
  62. package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
  63. package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
  64. package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
  65. package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
  66. package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
  67. package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
  68. package/dist/assets/web/dist/assets/{dist-BU5UivXC.js → dist-BtPs2AkQ.js} +2 -2
  69. package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
  70. package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
  71. package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
  72. package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
  73. package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
  74. package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
  75. package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
  76. package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
  77. package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
  78. package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
  79. package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
  80. package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
  81. package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
  82. package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
  83. package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
  84. package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
  85. package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
  86. package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +1 -0
  87. package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
  88. package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
  89. package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
  90. package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
  91. package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
  92. package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
  93. package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
  94. package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
  95. package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
  96. package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
  97. package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
  98. package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
  99. package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
  100. package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
  101. package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
  102. package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
  103. package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
  104. package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
  105. package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
  106. package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
  107. package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
  108. package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
  109. package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
  110. package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
  111. package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
  112. package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
  113. package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
  114. package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
  115. package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
  116. package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
  117. package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
  118. package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
  119. package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
  120. package/dist/assets/web/dist/assets/{sseReplay-g7ml86LM.js → sseReplay-DaRtc7nI.js} +5 -5
  121. package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
  122. package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
  123. package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
  124. package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
  125. package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
  126. package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
  127. package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
  128. package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
  129. package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
  130. package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
  131. package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
  132. package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
  133. package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
  134. package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
  135. package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
  136. package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
  137. package/dist/cli.js +5843 -2246
  138. package/package.json +1 -1
  139. package/dist/assets/web/dist/assets/CostsPage-CtmKhhOF.js +0 -2
  140. package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
  141. package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +0 -2
  142. package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +0 -1
  143. package/dist/assets/web/dist/assets/ResidentsIndexPage-CaDvXhzJ.js +0 -1
  144. package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +0 -1
  145. package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +0 -6
  146. package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +0 -1
  147. package/dist/assets/web/dist/assets/ScheduledPage-DXD2gLJk.js +0 -1
  148. package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +0 -1
  149. package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +0 -1
  150. package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
  151. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
  152. package/dist/assets/web/dist/assets/main-B2fX10aW.css +0 -1
  153. package/dist/assets/web/dist/assets/main-D4EA1g6n.js +0 -28
@@ -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
  }
@@ -0,0 +1,155 @@
1
+ // The renewal decision (decision 0046, Renewal: the grant decides, the lease
2
+ // continues). A lease is sized by the infrastructure; the problem is bounded by
3
+ // the grant. When a segment ends with its unit unfinished, the runner reads
4
+ // progress off the row — never off a model's word — and renews only when three
5
+ // things hold: the row shows progress, the grant has a renewal left and the
6
+ // spend is under its cap, and the pipeline's ask still passes the fit.
7
+ // Otherwise it stops and names the clause that failed; a person's reply spends
8
+ // a renewal by hand. Pure and node-free: the unit machine (a Worker) and the
9
+ // bot's routes compute the same decision from the same facts.
10
+
11
+ import { fit, type Grant, type Pipeline } from "../budgets.js";
12
+ import type { Handoff } from "./handoff.js";
13
+
14
+ /** A head the run pushed, as the record carries it (run-history item 2). */
15
+ export interface PushedHeadFact {
16
+ ref: string;
17
+ sha: string;
18
+ at?: number;
19
+ }
20
+
21
+ export interface ProgressInput {
22
+ /** The unit's branch: a push to any other ref is not the unit's progress. */
23
+ branch: string;
24
+ /** The heads the segment's coding run pushed (`RunRecord.pushed`). */
25
+ pushed: readonly PushedHeadFact[];
26
+ /** The head the segment started from — the previous segment's recorded sha.
27
+ * Absent on a fresh branch, where any push is progress. */
28
+ startHead?: string;
29
+ /** When the segment's lease began; a push stamped before it is not this segment's. */
30
+ leaseStartedAt?: number;
31
+ /** The previous segment's handoff and this one's, when both exist. */
32
+ handoff?: { previous: Handoff; current: Handoff };
33
+ }
34
+
35
+ export type Progress =
36
+ | { progressed: true; by: "push"; sha: string }
37
+ | { progressed: true; by: "handoff" }
38
+ | { progressed: false; why: string };
39
+
40
+ /** Two shas name one commit when the shorter is a prefix of the longer. */
41
+ const sameSha = (a: string, b: string): boolean => {
42
+ const x = a.toLowerCase();
43
+ const y = b.toLowerCase();
44
+ return x.length <= y.length ? y.startsWith(x) : x.startsWith(y);
45
+ };
46
+
47
+ /** Progress is a fact the row records after unit eight and the runner reads
48
+ * without a model: the last head pushed to the unit's branch that differs from
49
+ * the head the segment started at and is not older than the lease; failing
50
+ * that, a handoff whose follow-ups shrank or whose deviations grew. */
51
+ export function progressOf(input: ProgressInput): Progress {
52
+ const last = [...input.pushed].reverse().find((h) => h.ref === input.branch);
53
+ if (last !== undefined) {
54
+ const newerThanLease =
55
+ input.leaseStartedAt === undefined || last.at === undefined || last.at >= input.leaseStartedAt;
56
+ const moved = input.startHead === undefined || !sameSha(last.sha, input.startHead);
57
+ if (newerThanLease && moved) return { progressed: true, by: "push", sha: last.sha };
58
+ }
59
+ const pushWhy = `no head newer than the lease's start was pushed to \`${input.branch}\``;
60
+ if (input.handoff === undefined) return { progressed: false, why: pushWhy };
61
+ const { previous, current } = input.handoff;
62
+ if (current.followUps.length < previous.followUps.length || current.deviations.length > previous.deviations.length)
63
+ return { progressed: true, by: "handoff" };
64
+ return { progressed: false, why: `${pushWhy} and the handoff is unchanged` };
65
+ }
66
+
67
+ export interface RenewalInput {
68
+ grant: Grant;
69
+ /** Renewals already spent under this request: the segments beyond the first. */
70
+ renewalsSpent: number;
71
+ /** Dollars spent under this request so far; null when a run's model had no
72
+ * price, since a total that left a model's tokens out would understate it. */
73
+ spendUsd: number | null;
74
+ progress: Progress;
75
+ /** The pipeline the next segment would run: its fit is re-asserted before every segment. */
76
+ pipeline: Pipeline;
77
+ }
78
+
79
+ export type RenewalWhy = "no_progress" | "grant_exhausted" | "cost_cap" | "unfit";
80
+
81
+ export type RenewalDecision =
82
+ | { renew: true; segment: number; from?: string; renewalsLeft: number }
83
+ | { renew: false; why: RenewalWhy; detail: string; renewalsLeft: number };
84
+
85
+ const usd = (n: number): string => `$${n.toFixed(2)}`;
86
+
87
+ /** The decision, in the order the record states the clauses: progress, the
88
+ * grant's count, the cap, the fit. `segment` is the one the renewal opens
89
+ * (the first segment is 1); `from` the sha it continues from when a push made
90
+ * the progress. */
91
+ export function renewalDecision(input: RenewalInput): RenewalDecision {
92
+ const renewalsLeft = Math.max(0, input.grant.renewals - input.renewalsSpent);
93
+ if (!input.progress.progressed) return { renew: false, why: "no_progress", detail: input.progress.why, renewalsLeft };
94
+ if (renewalsLeft === 0) {
95
+ const detail =
96
+ input.grant.renewals === 0
97
+ ? "the grant holds no renewals"
98
+ : `the grant's ${input.grant.renewals} renewal${input.grant.renewals === 1 ? "" : "s"} are spent`;
99
+ return { renew: false, why: "grant_exhausted", detail, renewalsLeft };
100
+ }
101
+ const cap = input.grant.costCapUsd;
102
+ if (cap !== undefined) {
103
+ if (input.spendUsd === null)
104
+ return {
105
+ renew: false,
106
+ why: "cost_cap",
107
+ detail: `spend is unknown (a model had no price) under the grant's cap of $${cap}`,
108
+ renewalsLeft,
109
+ };
110
+ if (input.spendUsd >= cap)
111
+ return {
112
+ renew: false,
113
+ why: "cost_cap",
114
+ detail: `spend ${usd(input.spendUsd)} reached the grant's cap of $${cap}`,
115
+ renewalsLeft,
116
+ };
117
+ }
118
+ const held = fit(input.pipeline);
119
+ if (!held.ok)
120
+ return {
121
+ renew: false,
122
+ why: "unfit",
123
+ detail: `a ${held.have}-minute segment cannot hold the ship loop (${input.pipeline.maxRounds} review rounds need ${held.need} min)`,
124
+ renewalsLeft,
125
+ };
126
+ return {
127
+ renew: true,
128
+ segment: input.renewalsSpent + 2,
129
+ ...(input.progress.by === "push" ? { from: input.progress.sha } : {}),
130
+ renewalsLeft: renewalsLeft - 1,
131
+ };
132
+ }
133
+
134
+ /** The card's words (the record's trace, steps 4 and 9): a renewal names its
135
+ * number of the grant's and the sha it continues from; a stop names the
136
+ * clause and, when renewals remain, the reply that spends one by hand. */
137
+ export function renderRenewal(decision: RenewalDecision, grant: Grant): string {
138
+ if (decision.renew)
139
+ return `renewal ${decision.segment - 1} of ${grant.renewals}, continues ${decision.from !== undefined ? decision.from.slice(0, 7) : "the branch's head"}`;
140
+ const holds =
141
+ decision.renewalsLeft > 0
142
+ ? `grant holds ${decision.renewalsLeft} renewal${decision.renewalsLeft === 1 ? "" : "s"}`
143
+ : "the grant holds no renewals";
144
+ switch (decision.why) {
145
+ case "no_progress":
146
+ return decision.renewalsLeft > 0
147
+ ? `no progress in the last lease; ${holds}; reply continue to spend one`
148
+ : `no progress in the last lease; ${holds}`;
149
+ case "cost_cap":
150
+ return decision.renewalsLeft > 0 ? `${decision.detail}; ${holds} unspent` : decision.detail;
151
+ case "grant_exhausted":
152
+ case "unfit":
153
+ return decision.detail;
154
+ }
155
+ }
@@ -34,6 +34,14 @@ export interface AttrDomain {
34
34
  /** The `<provider>/<model>` that took the turn — a ship run's children answer
35
35
  * on different models, and the run page badges the switch per step. */
36
36
  model: string;
37
+ /** The tools a proxied call offered the model (model-proxy.md item 6): how
38
+ * many, their names sorted and comma-joined, and the request's `tool_choice`
39
+ * by its word — `auto` when tools came and nothing was said, `none` when no
40
+ * tools came or the request said so, `any` for OpenAI's `required`, `tool`
41
+ * for a named tool. */
42
+ tools: number;
43
+ toolNames: string;
44
+ toolChoice: "auto" | "none" | "any" | "tool";
37
45
  stopReason: "end_turn" | "tool_use" | "max_tokens" | "stop_sequence" | "other";
38
46
  inputTokens: number;
39
47
  outputTokens: number;
@@ -159,6 +167,9 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
159
167
  backend: "string",
160
168
  command: "string",
161
169
  model: "string",
170
+ tools: "number",
171
+ toolNames: "string",
172
+ toolChoice: "string",
162
173
  stopReason: "string",
163
174
  inputTokens: "number",
164
175
  outputTokens: "number",
@@ -70,9 +70,10 @@ export interface TraceOptions {
70
70
  span?: Span;
71
71
  /** Which door the call came through when it was not the surface's own
72
72
  * grammar: `route` for a command the request router bound from prose
73
- * (record 0036). Copied onto the registry's audit line, never read by a
74
- * handler. Absent for a typed command. */
75
- source?: "route";
73
+ * (record 0036), `confirm` for a stored input run at a confirmation's
74
+ * click (record 0044). Copied onto the registry's audit line, never read
75
+ * by a handler. Absent for a typed command. */
76
+ source?: "route" | "confirm";
76
77
  }
77
78
 
78
79
  export interface Span {
@@ -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
@@ -186,6 +186,22 @@ export function killStaleBuildProcessesCommand(user: string, dir: string): strin
186
186
  * failure however it died. */
187
187
  const INTERRUPTION_SIGNATURE = /\bexit 143\b|Session terminated|SIGTERM|^restore-interrupted:/;
188
188
 
189
+ /** The wordings git gives when the MIRROR itself is broken: its `origin`
190
+ * remote gone from `config`, the repository not a repository, an object store
191
+ * with bad, corrupt or unreadable objects, a local file it cannot open. None
192
+ * of these is GitHub's doing, and none is cured by waiting for GitHub. */
193
+ const MIRROR_FAILURE_WORDING =
194
+ /'origin' does not appear to be a git repository|not a git repository|\bbad object\b|\bcorrupt\b|unable to read (?:tree|object|commit|blob)|object file .* is empty|cannot open \.git\/|unable to open object|missing object/i;
195
+
196
+ /** Whether a failed mirror fetch failed on the mirror (the resident's own
197
+ * disk — item 67 counts it) rather than on GitHub (`github-unreachable`, a
198
+ * serviceable reason that parks on repeat, item 16b). GitHub is the default:
199
+ * a message names the mirror only through the wordings above, so an unknown
200
+ * failure keeps the reading it always had. */
201
+ export function fetchFailureIsMirrors(message: string): boolean {
202
+ return MIRROR_FAILURE_WORDING.test(message);
203
+ }
204
+
189
205
  /** Message wording of the Sandbox SDK's runtime-replacement error family — the
190
206
  * container went away UNDER a live SDK call, so the failure never reaches the
191
207
  * shell-kill signature above: a step that dies this way surfaces as
@@ -209,9 +225,34 @@ const INTERRUPTION_SIGNATURE = /\bexit 143\b|Session terminated|SIGTERM|^restore
209
225
  * exec path and the refresh classifier agree on one wording list (a container
210
226
  * stop mid-snapshot produces "Process supervisor is closed"; classified as the
211
227
  * repo's own `snapshot-failed` it would end the instance `degraded` instead of
212
- * being retried into the same step). */
213
- export const RUNTIME_REPLACEMENT_WORDING =
214
- /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;
228
+ * being retried into the same step).
229
+ *
230
+ * The list is the union of two a reader tells apart, because the resident's
231
+ * `/exec` answer treats them differently (resident-repos item 43): the SDK's
232
+ * words that vouch the runtime MOVED — a new incarnation serves the thread,
233
+ * so the container the command was aimed at is gone whatever else is known
234
+ * — and the platform's words for a container that is merely DOWN, which say
235
+ * nothing about a replacement: a stopped container answers "Process
236
+ * supervisor is closed" to a spawn, and the workerd binding answers "The
237
+ * container is not running" whether the container is asleep, starting, or
238
+ * gone under a roll. For the refresh classifier and `isRuntimeReplacement`
239
+ * the union is right: a step interrupted either way is retried onto the
240
+ * container that comes back. */
241
+ export const RUNTIME_MOVED_WORDING =
242
+ /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;
243
+ /** The platform's words for a container that is down under a command (see
244
+ * `RUNTIME_MOVED_WORDING`): the spawn refusal of a stopped container and the
245
+ * binding's not-running refusal, anchored to the full phrase so a genuine
246
+ * crash ("container exited with unexpected exit code") and the readiness
247
+ * probe ("the container is not listening") never match. The harness's
248
+ * container seam reads the same two as a container down under its one more
249
+ * command — a wait, never a judgement (harness.md item 6). */
250
+ export const STOPPED_CONTAINER_WORDING =
251
+ /process supervisor is closed|container is not running, consider calling start/i;
252
+ export const RUNTIME_REPLACEMENT_WORDING = new RegExp(
253
+ `${RUNTIME_MOVED_WORDING.source}|${STOPPED_CONTAINER_WORDING.source}`,
254
+ "i",
255
+ );
215
256
 
216
257
  /** `runtimeUnreachable` is the caller's word that the failure was the SDK's
217
258
  * connect abort at the exec choke point (the Worker's typed check over the
@@ -314,10 +355,14 @@ export const WAKE_PORT_READY_MS = 3 * 60_000;
314
355
  * SDK by a test. */
315
356
  export const SDK_RUNTIME_RECORD_KEY = "currentRuntimeIdentity";
316
357
 
317
- /** The DOMException the SDK's connect abort raises: `AbortError`, message
318
- * `The operation was aborted`. Anchored on the whole sentence so a command's
319
- * own `Aborted (core dumped)` never matches. */
320
- export const RUNTIME_UNREACHABLE_WORDING = /\bThe operation was aborted\b/;
358
+ /** The DOMException the SDK's connect abort raises — `fetchUpgradeAttempt`
359
+ * calls `controller.abort()` with no reason when `DEFAULT_CONNECT_TIMEOUT_MS`
360
+ * passes, so the runtime's own `AbortError` with the message exactly `The
361
+ * operation was aborted`. Anchored on the WHOLE message, so a command's own
362
+ * `Aborted (core dumped)` never matches and neither does a `TimeoutError`'s
363
+ * `The operation was aborted due to timeout` — the text `AbortSignal.timeout`
364
+ * raises, a route's own bound on a slow GitHub, not a silent runtime. */
365
+ export const RUNTIME_UNREACHABLE_WORDING = /^The operation was aborted\.?$/;
321
366
 
322
367
  /** Is this one link of an error's cause chain the SDK's connect abort? By
323
368
  * name first (the DOMException's `AbortError`), by the DOMException's message
@@ -49,7 +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 objects (fault injection)",
52
+ "break-mirror": "removing the mirror's config (fault injection)",
53
53
  "unmount-restores": "unmounting earlier restores",
54
54
  "mirror-restore-extract": "extracting the mirror snapshot",
55
55
  "checkout-restore-extract": "extracting the checkout snapshot",
@@ -38,7 +38,12 @@ export function residentErrorText(text: string): string {
38
38
  return redactAndCap(stripAnsi(text), RESIDENT_ERROR_CAP);
39
39
  }
40
40
 
41
- const SANITIZED_FIELDS = ["error", "reason", "summary"] as const;
41
+ /** The free-text fields a resident body carries: `error` (the reply), `reason`
42
+ * (the answer's own word or a lifecycle reason), `stateReason` (the lifecycle
43
+ * reason beside `state` on a thread route's 503 — the same remote text
44
+ * `reason` used to carry there, item 62), `summary`. A field added to a
45
+ * resident answer that carries remote output is added HERE first. */
46
+ const SANITIZED_FIELDS = ["error", "reason", "stateReason", "summary"] as const;
42
47
 
43
48
  /** How deep the sanitizer descends. The deepest resident shape today is
44
49
  * `/residents` → `residents[]` → `live` → `reason` (depth 3); the bound exists
@@ -48,8 +53,8 @@ const SANITIZE_DEPTH = 4;
48
53
  /** A parsed resident body with its free-text fields made safe, at every level:
49
54
  * `/residents` nests each resident's `state`/`reason` (or an `error`) under
50
55
  * `residents[].live`, and `repo list` renders those. In each plain object only
51
- * `error`, `reason` and `summary` are touched (when strings) — `error` at the
52
- * reply-sized cap, the other two at the card-sized one; `stderr` is
56
+ * `error`, `reason`, `stateReason` and `summary` are touched (when strings) —
57
+ * `error` at the reply-sized cap, the others at the card-sized one; `stderr` is
53
58
  * rewritten only when it mirrors `error` (the thread routes' failure shape
54
59
  * copies the error into stderr). Every other field — `needs`, `state`,
55
60
  * `stdout`, `status`, bindings, numbers — passes through untouched. Arrays are
@@ -238,6 +238,9 @@ export interface McpServerView {
238
238
  promotedFromName?: string;
239
239
  /** A user row's owner (the id after `user:` in `scopeKey`) as a display name, when the lookup answered. */
240
240
  ownerName?: string;
241
+ /** A channel row's channel (the id after `channel:` in `scopeKey`) as its name without the hash,
242
+ * when the lookup answered. */
243
+ channelName?: string;
241
244
  /** A higher tier holds this name, so runs use that entry and this one is idle (`mcpServersFor`'s
242
245
  * `shadowedBy`, the winning scope key — `org`, or `channel:<id>`); absent when this entry serves. */
243
246
  shadowedBy?: string;