@coreplane/switchboard 1.245.0 → 1.246.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 (123) hide show
  1. package/dist/assets/deploy/cloudflare-resident/worker.ts +9 -10
  2. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +17 -1
  3. package/dist/assets/package-lock.json +3 -3
  4. package/dist/assets/package.json +1 -1
  5. package/dist/assets/source.json +3 -3
  6. package/dist/assets/src/agents/registry.ts +31 -7
  7. package/dist/assets/src/core/budgets.ts +18 -0
  8. package/dist/assets/src/core/coordinator/contract.ts +29 -4
  9. package/dist/assets/src/core/coordinator/driver.ts +7 -1
  10. package/dist/assets/src/core/reviewVerdict.ts +138 -6
  11. package/dist/assets/src/core/runEvents.ts +29 -1
  12. package/dist/assets/src/core/runRecord.ts +38 -16
  13. package/dist/assets/src/core/ship/coordinator.ts +23 -2
  14. package/dist/assets/src/core/ship/renewal.ts +10 -3
  15. package/dist/assets/src/execution/bashTimeout.ts +46 -2
  16. package/dist/assets/src/execution/residentRefresh.ts +9 -5
  17. package/dist/assets/src/execution/sandboxErrors.ts +9 -4
  18. package/dist/assets/web/dist/.vite/manifest.json +468 -468
  19. package/dist/assets/web/dist/assets/{AppShell-qqs_p96-.js → AppShell-B4EUiSwP.js} +1 -1
  20. package/dist/assets/web/dist/assets/{CostsPage-Dd9aXxxI.js → CostsPage-D8VjAtuH.js} +1 -1
  21. package/dist/assets/web/dist/assets/DeliveryPage-DpQi5XUI.js +1 -0
  22. package/dist/assets/web/dist/assets/{HomePage-VLTFopnl.js → HomePage-CvKuIGqg.js} +1 -1
  23. package/dist/assets/web/dist/assets/{InputMenu-lQP3ia9D.js → InputMenu-DmqAxkn4.js} +1 -1
  24. package/dist/assets/web/dist/assets/{NotFoundPage-8ViskpN3.js → NotFoundPage-DDogyiXA.js} +1 -1
  25. package/dist/assets/web/dist/assets/{PendingTurnRow-D2gHef5v.js → PendingTurnRow-BoSLlR6j.js} +1 -1
  26. package/dist/assets/web/dist/assets/{ResidentDetailPage-nWLN8BOd.js → ResidentDetailPage-CiBdbzkE.js} +1 -1
  27. package/dist/assets/web/dist/assets/{ResidentsIndexPage-D3Q48CJW.js → ResidentsIndexPage-DgtHtd9K.js} +1 -1
  28. package/dist/assets/web/dist/assets/{RunFoldRow-CHy_IfZG.js → RunFoldRow-IF4xbz-3.js} +1 -1
  29. package/dist/assets/web/dist/assets/{RunRoutePage-C6kX5om4.js → RunRoutePage-DHYa7rHK.js} +3 -3
  30. package/dist/assets/web/dist/assets/RunsIndexPage-aWhNTv8r.js +1 -0
  31. package/dist/assets/web/dist/assets/{RunsTabs-WyeG5tKb.js → RunsTabs-CY_tdGTU.js} +1 -1
  32. package/dist/assets/web/dist/assets/{ScheduledPage-B8D5AU4d.js → ScheduledPage-gPEBZjcx.js} +1 -1
  33. package/dist/assets/web/dist/assets/{SettingSelect-UTHsqUUS.js → SettingSelect-VDanrTZi.js} +1 -1
  34. package/dist/assets/web/dist/assets/{SettingsPage-BoW7Zjzt.js → SettingsPage-CFoltSRD.js} +1 -1
  35. package/dist/assets/web/dist/assets/{StatusDot-BGHureCJ.js → StatusDot-DA3eTVrT.js} +1 -1
  36. package/dist/assets/web/dist/assets/{Tooltip-CiP8qPd7.js → Tooltip-BgbCkM12.js} +1 -1
  37. package/dist/assets/web/dist/assets/{UnitRoutePage-2zEeRBP1.js → UnitRoutePage-DibegaJJ.js} +1 -1
  38. package/dist/assets/web/dist/assets/{angular-html-qXhb9ti8.js → angular-html-D_3slZqf.js} +1 -1
  39. package/dist/assets/web/dist/assets/{angular-ts-DzvdyoUZ.js → angular-ts-D1Fv2cpN.js} +1 -1
  40. package/dist/assets/web/dist/assets/{apl-Ck7Ru2Sm.js → apl-60Nm_trN.js} +1 -1
  41. package/dist/assets/web/dist/assets/{astro-CwehvReb.js → astro-CvyaM3Q9.js} +1 -1
  42. package/dist/assets/web/dist/assets/{blade-WSyVbSFK.js → blade-yan3VBls.js} +1 -1
  43. package/dist/assets/web/dist/assets/{c-DMHI1wDz.js → c-Ddgepap2.js} +1 -1
  44. package/dist/assets/web/dist/assets/{chapel-BF8AYK8_.js → chapel-CgNY6dxC.js} +1 -1
  45. package/dist/assets/web/dist/assets/{cobol-UAfImuWu.js → cobol-C7q82cdP.js} +1 -1
  46. package/dist/assets/web/dist/assets/{coffee-D54hi96d.js → coffee-DgRMF_ni.js} +1 -1
  47. package/dist/assets/web/dist/assets/{cpp-BRWfLgBc.js → cpp-B8Lf-aeo.js} +1 -1
  48. package/dist/assets/web/dist/assets/{crystal-CSqC5deT.js → crystal-CkA_dhEO.js} +1 -1
  49. package/dist/assets/web/dist/assets/{css-D4ZuRFGk.js → css-Cyu-C_oA.js} +1 -1
  50. package/dist/assets/web/dist/assets/{dist-73ZMn_Mg.js → dist-DS-gMjLZ.js} +2 -2
  51. package/dist/assets/web/dist/assets/{durationTone-D51enaGR.js → durationTone-l6aVrHb8.js} +1 -1
  52. package/dist/assets/web/dist/assets/{edge-DDPtNbZc.js → edge-BxV3rKNI.js} +1 -1
  53. package/dist/assets/web/dist/assets/{elixir-BTQkQfsJ.js → elixir-pNSEK39d.js} +1 -1
  54. package/dist/assets/web/dist/assets/{elm-B_PkLzGZ.js → elm-zgwWE33W.js} +1 -1
  55. package/dist/assets/web/dist/assets/{erb-DfjIMVM_.js → erb-yzcYAHp_.js} +1 -1
  56. package/dist/assets/web/dist/assets/{eventSource-By0B_w4S.js → eventSource-Ce94ho6M.js} +1 -1
  57. package/dist/assets/web/dist/assets/{git-rebase-DP8wmn7K.js → git-rebase-Dg7Ous1-.js} +1 -1
  58. package/dist/assets/web/dist/assets/{glimmer-js-G12VK2gK.js → glimmer-js-CbZEwX27.js} +1 -1
  59. package/dist/assets/web/dist/assets/{glimmer-ts-D3XiSI8E.js → glimmer-ts-D3NIqALy.js} +1 -1
  60. package/dist/assets/web/dist/assets/{glsl-DOKAhMj-.js → glsl-y4-3WrI2.js} +1 -1
  61. package/dist/assets/web/dist/assets/{graphql-CJB_fmON.js → graphql-D5d0al7K.js} +1 -1
  62. package/dist/assets/web/dist/assets/{hack-BHqtRWap.js → hack-D8fD6kEl.js} +1 -1
  63. package/dist/assets/web/dist/assets/{haml-DwotyarS.js → haml-BWeFFc-V.js} +1 -1
  64. package/dist/assets/web/dist/assets/{handlebars-B6fDaA3U.js → handlebars-gIm5FeE_.js} +1 -1
  65. package/dist/assets/web/dist/assets/{html-derivative-C5bpQmmI.js → html-derivative-B3ySSBrQ.js} +1 -1
  66. package/dist/assets/web/dist/assets/{html-s52taj36.js → html-fXJTVLSM.js} +1 -1
  67. package/dist/assets/web/dist/assets/{http-99zC935r.js → http-DrJfQUmw.js} +1 -1
  68. package/dist/assets/web/dist/assets/{hurl-Bgk-4-0x.js → hurl-Bvbnsf55.js} +1 -1
  69. package/dist/assets/web/dist/assets/{java-CuV_c5I0.js → java-CUSpb-i-.js} +1 -1
  70. package/dist/assets/web/dist/assets/{javascript-CZlOvSMQ.js → javascript-BWRH5fbY.js} +1 -1
  71. package/dist/assets/web/dist/assets/{jinja-BuXeZ4Tu.js → jinja-D3Ymci8y.js} +1 -1
  72. package/dist/assets/web/dist/assets/{jison-DIuP0hkG.js → jison-Gt9SO7ZC.js} +1 -1
  73. package/dist/assets/web/dist/assets/{json-CZ74vfoD.js → json-Cfcblhgn.js} +1 -1
  74. package/dist/assets/web/dist/assets/{jsx-BNjRs8Vj.js → jsx-C0366fFM.js} +1 -1
  75. package/dist/assets/web/dist/assets/{julia-OCE-fFOg.js → julia-CyBOTbxS.js} +1 -1
  76. package/dist/assets/web/dist/assets/{just-CtvD87WF.js → just-FPXZcIOd.js} +1 -1
  77. package/dist/assets/web/dist/assets/{latex-C3kf86OR.js → latex-Bd-c88v0.js} +1 -1
  78. package/dist/assets/web/dist/assets/{liquid-B1WI--Kp.js → liquid-5tu9Hc0k.js} +1 -1
  79. package/dist/assets/web/dist/assets/{lua-DtoT0gKg.js → lua-BZ5fdPP8.js} +1 -1
  80. package/dist/assets/web/dist/assets/{main-ClluP-Qf.js → main-B4A6x82d.js} +3 -3
  81. package/dist/assets/web/dist/assets/{marko-BE2MC6Yt.js → marko-C_mmj4p0.js} +1 -1
  82. package/dist/assets/web/dist/assets/{mdc-BwO4weXJ.js → mdc-CtaNqWjY.js} +1 -1
  83. package/dist/assets/web/dist/assets/{nginx-BTuOgGVC.js → nginx-Ddm5Mb8J.js} +1 -1
  84. package/dist/assets/web/dist/assets/{nim-CiIUM-oV.js → nim-COhfroYB.js} +1 -1
  85. package/dist/assets/web/dist/assets/{org-L78DEWB-.js → org-CCHFgmTb.js} +1 -1
  86. package/dist/assets/web/dist/assets/{perl-KqHOm4Ft.js → perl-CXzOE-Z-.js} +1 -1
  87. package/dist/assets/web/dist/assets/{php-C41XbEkn.js → php-1V6jAIhU.js} +1 -1
  88. package/dist/assets/web/dist/assets/{pug-B12-4i-k.js → pug-uS37efts.js} +1 -1
  89. package/dist/assets/web/dist/assets/{qml--fw2Up5c.js → qml-Bj1A0Ben.js} +1 -1
  90. package/dist/assets/web/dist/assets/{r-Db1JBXrC.js → r-CMbKbLsS.js} +1 -1
  91. package/dist/assets/web/dist/assets/{razor-BYpVBgbu.js → razor-DVRq_Ndk.js} +1 -1
  92. package/dist/assets/web/dist/assets/{regexp-BMqGLSPc.js → regexp-AbaOr4oB.js} +1 -1
  93. package/dist/assets/web/dist/assets/{rst-B63n7-Sa.js → rst-Dq_QWgOF.js} +1 -1
  94. package/dist/assets/web/dist/assets/{ruby-D3VI8UYv.js → ruby-CVSsAj7T.js} +1 -1
  95. package/dist/assets/web/dist/assets/{sas-DxRf8ZAR.js → sas-C-dGfxUi.js} +1 -1
  96. package/dist/assets/web/dist/assets/{scss-DRRA6d6A.js → scss-DxALxYPA.js} +1 -1
  97. package/dist/assets/web/dist/assets/{seed-DFfT5S_N.js → seed-BhxEVasY.js} +1 -1
  98. package/dist/assets/web/dist/assets/{shellscript-CoOBe3_y.js → shellscript--qXcvQ5L.js} +1 -1
  99. package/dist/assets/web/dist/assets/{shellsession-DOV1HYxf.js → shellsession-DeqnW8Ux.js} +1 -1
  100. package/dist/assets/web/dist/assets/{soy-Acpmpp1W.js → soy-DreoUIMt.js} +1 -1
  101. package/dist/assets/web/dist/assets/{sql-DeT9N-fA.js → sql-o3i2VTG6.js} +1 -1
  102. package/dist/assets/web/dist/assets/{sseReplay-DPXVxhL5.js → sseReplay-qF-gpbAY.js} +1 -1
  103. package/dist/assets/web/dist/assets/{stata-CbxXAMO4.js → stata-CGAT41H7.js} +1 -1
  104. package/dist/assets/web/dist/assets/{surrealql-BdKbi4hc.js → surrealql-CBU0QuGV.js} +1 -1
  105. package/dist/assets/web/dist/assets/{svelte-tAcjDM7R.js → svelte-CfHaLZiG.js} +1 -1
  106. package/dist/assets/web/dist/assets/{templ-CObINNJj.js → templ-D02MUcma.js} +1 -1
  107. package/dist/assets/web/dist/assets/{tex-BYwpZT1J.js → tex-DjprHYXl.js} +1 -1
  108. package/dist/assets/web/dist/assets/{ts-tags-BciREL5R.js → ts-tags-DftDOzP-.js} +1 -1
  109. package/dist/assets/web/dist/assets/{tsx-CRiv3Pr3.js → tsx-BOdvPohJ.js} +1 -1
  110. package/dist/assets/web/dist/assets/{twig-DuZZ1Ej0.js → twig-B_yLS56B.js} +1 -1
  111. package/dist/assets/web/dist/assets/{typescript-jkHz_5HZ.js → typescript-CdPCa4nU.js} +1 -1
  112. package/dist/assets/web/dist/assets/{typst-DOac4Gal.js → typst-C1oIFsUl.js} +1 -1
  113. package/dist/assets/web/dist/assets/{vue-CgOX5wXA.js → vue-CcszJEX8.js} +1 -1
  114. package/dist/assets/web/dist/assets/{vue-html-Duu99VNw.js → vue-html-DcpxPcuT.js} +1 -1
  115. package/dist/assets/web/dist/assets/{vue-vine-Bmj_wvRJ.js → vue-vine-D26bIKGj.js} +1 -1
  116. package/dist/assets/web/dist/assets/{wallClock-DD_fPuSY.js → wallClock-DPTuP2FD.js} +1 -1
  117. package/dist/assets/web/dist/assets/{xml-BtUIVeWH.js → xml-D8zl6W5P.js} +1 -1
  118. package/dist/assets/web/dist/assets/{xsl-wX71n1E5.js → xsl-D86Bii28.js} +1 -1
  119. package/dist/assets/web/dist/assets/{yaml-CdLdXvmz.js → yaml-g6Px4KFU.js} +1 -1
  120. package/dist/cli.js +1750 -566
  121. package/package.json +1 -1
  122. package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +0 -1
  123. package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +0 -1
@@ -291,6 +291,16 @@ export function pushedBranchesOf(events: readonly RunEvent[]): PushedBranch[] {
291
291
  return [...byRef].map(([ref, pr]) => ({ ref, pr }));
292
292
  }
293
293
 
294
+ /** A call a run's ending may have left running in its workspace (harness.md
295
+ * item 13): the id the record pairs by, the tool, and the line the
296
+ * `workspace_torn_down` note names it by — the `tool_call`'s own summary
297
+ * while the record holds it, else the tool and the id. */
298
+ export interface CallInFlight {
299
+ callId: string;
300
+ tool: string;
301
+ summary: string;
302
+ }
303
+
294
304
  /** The tool calls a run's ending may have left running in its workspace, in
295
305
  * the record's order — what the workspace's release reads to tear the
296
306
  * workspace down rather than wait for an idle the command would refuse, and
@@ -299,26 +309,38 @@ export function pushedBranchesOf(events: readonly RunEvent[]): PushedBranch[] {
299
309
  * settled, whatever the run's status, and stays cut whatever lands for it
300
310
  * later — an unmarked result (a post-turn's bridge reads the earlier
301
311
  * execution's aborted settle as its own), or the call announced again under
302
- * the same id; a call with no result at
303
- * all was left open by a run that failed, was interrupted or hard-stopped,
304
- * while on a run that completed or stopped softly it is a relayed tool that
305
- * ran in the bot or a result lost to a transport gap, nothing running. Calls
306
- * and results pair by id; a result for a call the record never opened counts
307
- * for nothing. */
308
- export function callsInFlight(
309
- events: readonly RunEvent[],
310
- status: RunStatus,
311
- ): Extract<RunEvent, { type: "tool_call" }>[] {
312
- const calls = new Map<
313
- string,
314
- { call: Extract<RunEvent, { type: "tool_call" }>; state: "open" | "cut" | "settled" }
315
- >();
312
+ * the same id; a call with no result at all was left open by a run that
313
+ * failed, was interrupted or hard-stopped, while on a run that completed or
314
+ * stopped softly it is a relayed tool that ran in the bot or a result lost to
315
+ * a transport gap, nothing running. Calls and results pair by id. The events
316
+ * are the registry's bounded backlog (`runRegistry/backlog.ts` drops the
317
+ * oldest past its count or byte bound), so on a long run the line of a
318
+ * command that hung early may be gone while its cut result — the ending's,
319
+ * landing last — survives: a cut result for a call the record no longer
320
+ * holds is the harness's own word that the call was ended and not settled,
321
+ * and counts by itself, named by its tool and id; an unmarked result for a
322
+ * call never opened counts for nothing. */
323
+ export function callsInFlight(events: readonly RunEvent[], status: RunStatus): CallInFlight[] {
324
+ const calls = new Map<string, { call: CallInFlight; state: "open" | "cut" | "settled" }>();
316
325
  for (const e of events) {
317
326
  if (e.type === "tool_call" && e.callId !== undefined) {
318
- if (calls.get(e.callId)?.state !== "cut") calls.set(e.callId, { call: e, state: "open" });
327
+ const seen = calls.get(e.callId);
328
+ if (seen?.state === "cut") {
329
+ // Announced again after its cut — or after an orphan cut, the line the
330
+ // backlog evicted now written again: the line is the record's, the
331
+ // state stays the cut's.
332
+ seen.call.tool = e.tool;
333
+ seen.call.summary = e.summary;
334
+ } else calls.set(e.callId, { call: { callId: e.callId, tool: e.tool, summary: e.summary }, state: "open" });
319
335
  } else if (e.type === "tool_result" && e.callId !== undefined) {
320
336
  const seen = calls.get(e.callId);
321
- if (seen !== undefined && seen.state !== "cut") seen.state = e.cut === true ? "cut" : "settled";
337
+ if (seen === undefined) {
338
+ if (e.cut === true)
339
+ calls.set(e.callId, {
340
+ call: { callId: e.callId, tool: e.tool, summary: `${e.tool} (call ${e.callId})` },
341
+ state: "cut",
342
+ });
343
+ } else if (seen.state !== "cut") seen.state = e.cut === true ? "cut" : "settled";
322
344
  }
323
345
  }
324
346
  const orderly = status === "completed" || status === "stopped_soft";
@@ -600,8 +600,22 @@ export type UnitEnding =
600
600
  /** What a transition tells the driver beyond the next action: a round boundary
601
601
  * the card draws (`shipRoundHeader`) and the run stream records, and the end. */
602
602
  export type CoordinatorNote =
603
- | { type: "round"; index: number; agent: ChildPreset; outcome: ShipRoundOutcome }
603
+ | {
604
+ type: "round";
605
+ index: number;
606
+ agent: ChildPreset;
607
+ outcome: ShipRoundOutcome;
608
+ /** The severity gate fired on this approve (agent-ship item 9): the
609
+ * findings at or above the level in force, as `id (severity)`. The
610
+ * child's own verdict parser holds an approve to the same level
611
+ * (agent-review item 5a), so this should never be set — when it is, the
612
+ * verdict was parsed at another level (a lost directive, an older
613
+ * record, a harness around `submit_verdict`) and the row, the card and
614
+ * the log say so instead of routing silently into the findings step. */
615
+ gate?: { level: AddressSeverity; findings: string[] };
616
+ }
604
617
  | { type: "ended"; ending: UnitEnding };
618
+ type RoundNote = Extract<CoordinatorNote, { type: "round" }>;
605
619
 
606
620
  /** How the pipeline's budget went, in ms: the coding rounds' (round 0 and the
607
621
  * findings steps), the review rounds', and everything else (branching,
@@ -939,7 +953,10 @@ function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[]
939
953
  return { state: { ...s, phase: ENDED, ending }, notes: [...notes, { type: "ended", ending }] };
940
954
  }
941
955
 
942
- const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): CoordinatorNote => ({
956
+ /** A gated finding as the gate note names it: `F1 (minor)`. */
957
+ const gateLabel = (f: Finding): string => `${f.id} (${f.severity})`;
958
+
959
+ const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): RoundNote => ({
943
960
  type: "round",
944
961
  index: round.index,
945
962
  agent: presetOf(round.kind),
@@ -1114,6 +1131,10 @@ function settleReview(
1114
1131
  const level = next.input.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
1115
1132
  const gated = findingsAtOrAbove(verdict.findings, level);
1116
1133
  if (gated.length > 0) {
1134
+ // The gate fired — which the child's parser should have made impossible
1135
+ // (agent-review item 5a): the round note carries what it caught, so the
1136
+ // mismatch is seen and not just routed around.
1137
+ notes[0] = { ...roundNote(round, verdict.verdict), gate: { level, findings: gated.map(gateLabel) } };
1117
1138
  if (mode !== undefined)
1118
1139
  return end(
1119
1140
  next,
@@ -24,8 +24,12 @@ export interface ProgressInput {
24
24
  /** The heads the segment's coding run pushed (`RunRecord.pushed`). */
25
25
  pushed: readonly PushedHeadFact[];
26
26
  /** The head the segment started from — the previous segment's recorded sha.
27
- * Absent on a fresh branch, where any push is progress. */
27
+ * Absent on a fresh branch, where the base head stands in for it. */
28
28
  startHead?: string;
29
+ /** The base branch's head the unit's branch was created at. On a fresh
30
+ * branch (no `startHead`) a push of this head is the branch's creation,
31
+ * not the unit's progress; with neither known, any push is progress. */
32
+ baseHead?: string;
29
33
  /** When the segment's lease began; a push stamped before it is not this segment's. */
30
34
  leaseStartedAt?: number;
31
35
  /** The previous segment's handoff and this one's, when both exist. */
@@ -53,7 +57,10 @@ export function progressOf(input: ProgressInput): Progress {
53
57
  if (last !== undefined) {
54
58
  const newerThanLease =
55
59
  input.leaseStartedAt === undefined || last.at === undefined || last.at >= input.leaseStartedAt;
56
- const moved = input.startHead === undefined || !sameSha(last.sha, input.startHead);
60
+ // A fresh branch starts at the base head, so that head is the bar a push
61
+ // must move past when no segment recorded a start.
62
+ const startHead = input.startHead ?? input.baseHead;
63
+ const moved = startHead === undefined || !sameSha(last.sha, startHead);
57
64
  if (newerThanLease && moved) return { progressed: true, by: "push", sha: last.sha };
58
65
  }
59
66
  const pushWhy = `no head newer than the lease's start was pushed to \`${input.branch}\``;
@@ -95,7 +102,7 @@ export function renewalDecision(input: RenewalInput): RenewalDecision {
95
102
  const detail =
96
103
  input.grant.renewals === 0
97
104
  ? "the grant holds no renewals"
98
- : `the grant's ${input.grant.renewals} renewal${input.grant.renewals === 1 ? "" : "s"} are spent`;
105
+ : `the grant's ${input.grant.renewals} renewal${input.grant.renewals === 1 ? " is" : "s are"} spent`;
99
106
  return { renew: false, why: "grant_exhausted", detail, renewalsLeft };
100
107
  }
101
108
  const cap = input.grant.costCapUsd;
@@ -4,7 +4,7 @@
4
4
  // Deliberately free of node: imports so wrangler can bundle it into Workers.
5
5
  // The numbers are rows of src/core/budgets.ts (docs/decisions/0046); this file
6
6
  // keeps their names for the readers that learned them here.
7
- import { ALLOWANCES, BASH_COMMAND, MINUTE_MS } from "../core/budgets.js";
7
+ import { ALLOWANCES, ATTACH_REQUEST_MIN_MS, BASH_COMMAND, MINUTE_MS } from "../core/budgets.js";
8
8
 
9
9
  /** Default per-command budget when the caller passes no timeoutMs. */
10
10
  export const BASH_TIMEOUT_MS = BASH_COMMAND.defaultMinutes * MINUTE_MS;
@@ -50,8 +50,10 @@ export function clampBashTimeout(requested: unknown): number {
50
50
  export type RunBudget =
51
51
  { kind: "unchanged" } | { kind: "clipped"; timeoutMs: number; note: string } | { kind: "exhausted"; note: string };
52
52
 
53
+ /** Whole seconds for the notes below, never negative: the two clip notes phrase the reserve alike. */
54
+ const secs = (ms: number): number => Math.max(0, Math.round(ms / 1000));
55
+
53
56
  export function bashBudgetWithinRun(wantedMs: number, remainingMs: number): RunBudget {
54
- const secs = (ms: number) => Math.max(0, Math.round(ms / 1000));
55
57
  const cap = Math.trunc(remainingMs - RUN_DEADLINE_RESERVE_MS);
56
58
  if (cap < BASH_TIMEOUT_MIN_MS) {
57
59
  return {
@@ -69,6 +71,48 @@ export function bashBudgetWithinRun(wantedMs: number, remainingMs: number): RunB
69
71
  };
70
72
  }
71
73
 
74
+ /** The bound on an attach request an executor opens while its run has
75
+ * `remainingMs` of wall clock left (`ResidentExecutorOptions.remainingMs`):
76
+ * `bounded` by the attach's own default, `BASH_TIMEOUT_MS` — an attach may
77
+ * clone and install deps — clipped to what the run has left less the same
78
+ * write-up reserve a command keeps back, so an attach never holds a run past
79
+ * its lease and the model keeps its write-up; never raised above the default.
80
+ * Inside the reserve, or with less than an attach's floor past it
81
+ * (`ATTACH_REQUEST_MIN_MS`: a bound too short to clone would be cut and
82
+ * struck as a rollout), `exhausted`: the request is not opened at all — the
83
+ * resident would run the attach to its end server-side for a run that is
84
+ * ending, and the caller would get a deadline it could read as waitable — and
85
+ * the note names the run's clock and the bound that refused (the reserve, or
86
+ * the floor past it): it is the one sentence every reader carries, the
87
+ * executor's refusal and the relaunch's note alike. The command's own budget
88
+ * never bounds an attach: the attach is not the command, and the command's
89
+ * one-second floor is the model's to choose. */
90
+ export type AttachBound = { kind: "bounded"; timeoutMs: number } | { kind: "exhausted"; note: string };
91
+
92
+ export function attachBoundWithinRun(remainingMs: number): AttachBound {
93
+ const left = Math.trunc(remainingMs - RUN_DEADLINE_RESERVE_MS);
94
+ // The one sentence every reader of an exhausted bound carries — the
95
+ // executor's refusal, the relaunch's note — worded by the bound that refused.
96
+ if (left <= 0) {
97
+ return {
98
+ kind: "exhausted",
99
+ note:
100
+ `the run has ${secs(remainingMs)}s of wall clock left, inside the ${secs(RUN_DEADLINE_RESERVE_MS)}s ` +
101
+ "write-up reserve, so no attach was opened",
102
+ };
103
+ }
104
+ if (left < ATTACH_REQUEST_MIN_MS) {
105
+ return {
106
+ kind: "exhausted",
107
+ note:
108
+ `the run has ${secs(remainingMs)}s of wall clock left, only ${secs(left)}s past the ` +
109
+ `${secs(RUN_DEADLINE_RESERVE_MS)}s write-up reserve — under the ${secs(ATTACH_REQUEST_MIN_MS)}s an attach needs — ` +
110
+ "so no attach was opened",
111
+ };
112
+ }
113
+ return { kind: "bounded", timeoutMs: Math.min(BASH_TIMEOUT_MS, left) };
114
+ }
115
+
72
116
  /** The line a timed-out command shows the model: names the limit that fired
73
117
  * and the knob that raises it, so the model can self-correct (re-run with a
74
118
  * larger timeoutMs, split the command, or background it) instead of guessing
@@ -180,11 +180,15 @@ export function killStaleBuildProcessesCommand(user: string, dir: string): strin
180
180
  }
181
181
 
182
182
  /** Signature of a step killed from OUTSIDE its own budget: the exit status of
183
- * SIGTERM (128 + 15), bash's "Session terminated" on a killed login shell, or
184
- * a tool naming the signal. A bare "killed" is NOT enough — compilers and
185
- * OOM messages say it too — and a step the cycle itself timed out is a real
186
- * failure however it died. */
187
- const INTERRUPTION_SIGNATURE = /\bexit 143\b|Session terminated|SIGTERM|^restore-interrupted:/;
183
+ * SIGTERM (128 + 15) — as a shell reports it (`exit 143`) or as the platform
184
+ * words a container that died under a short exec ("Container exited with
185
+ * unexpected exit code: 143"; seen live on the wake path's ready-stamp probe
186
+ * when a stop landed on it) — bash's "Session terminated" on a killed login
187
+ * shell, or a tool naming the signal. Only 143: the platform's same wording
188
+ * with any other code (1, 137, 139) is a genuine crash and stays a failure.
189
+ * A bare "killed" is NOT enough — compilers and OOM messages say it too —
190
+ * and a step the cycle itself timed out is a real failure however it died. */
191
+ const INTERRUPTION_SIGNATURE = /\bexit 143\b|exit code: 143\b|Session terminated|SIGTERM|^restore-interrupted:/;
188
192
 
189
193
  /** The wordings git gives when the MIRROR itself is broken: its `origin`
190
194
  * remote gone from `config`, the repository not a repository, an object store
@@ -154,10 +154,15 @@ export const SANDBOX_STARTING_EXPLANATION =
154
154
  "the thread's sandbox container is starting (image pull, boot, runtime) — nothing ran yet; the request is re-sent once it is up";
155
155
 
156
156
  /** The executor waits at most this long for a container to start, whatever
157
- * the command's own budget: the platform's own start allowances (30 s for an
158
- * instance, 90 s for the port) plus a slow image pull fit inside it, and a
159
- * 60 s command is never killed by a two-minute start it did not cause. */
160
- export const SANDBOX_START_WAIT_MAX_MS = 5 * 60_000;
157
+ * the command's own budget: the platform's own start allowances (the
158
+ * instance grant, 90 s for the port) plus a slow image pull fit inside it,
159
+ * and a 60 s command is never killed by a two-minute start it did not cause.
160
+ * Ten minutes, not five: under a midday burst the platform admitted about
161
+ * seven starts at once and granted the rest 2.5 to 5 minutes later — two
162
+ * threads of twenty-four died at the five-minute mark with their container
163
+ * a minute away. A run that waits ten minutes for its container starts; one
164
+ * that dies at five does not, and nothing else in the run was in progress. */
165
+ export const SANDBOX_START_WAIT_MAX_MS = 10 * 60_000;
161
166
 
162
167
  /** Backoff between re-sends while a container starts: 5 s, 10 s, then 15 s.
163
168
  * A start takes tens of seconds, not minutes, so the poll is denser than the