@coreplane/switchboard 1.244.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 (150) hide show
  1. package/dist/assets/config/config.example.yaml +6 -3
  2. package/dist/assets/deploy/cloudflare/worker.ts +2 -0
  3. package/dist/assets/deploy/cloudflare-resident/threadErr.ts +337 -0
  4. package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
  5. package/dist/assets/deploy/cloudflare-resident/worker.ts +40 -272
  6. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +22 -11
  7. package/dist/assets/deploy/secrets.manifest.json +12 -0
  8. package/dist/assets/package-lock.json +3 -3
  9. package/dist/assets/package.json +1 -1
  10. package/dist/assets/source.json +3 -3
  11. package/dist/assets/src/agents/registry.ts +34 -10
  12. package/dist/assets/src/config/profile.ts +17 -1
  13. package/dist/assets/src/core/authz/types.ts +8 -0
  14. package/dist/assets/src/core/budgets.ts +43 -0
  15. package/dist/assets/src/core/coordinator/contract.ts +29 -4
  16. package/dist/assets/src/core/coordinator/driver.ts +7 -1
  17. package/dist/assets/src/core/reviewVerdict.ts +239 -30
  18. package/dist/assets/src/core/runEvents.ts +55 -2
  19. package/dist/assets/src/core/runRecord.ts +56 -0
  20. package/dist/assets/src/core/ship/coordinator.ts +48 -23
  21. package/dist/assets/src/core/ship/renewal.ts +10 -3
  22. package/dist/assets/src/execution/bashTimeout.ts +46 -2
  23. package/dist/assets/src/execution/residentRefresh.ts +34 -11
  24. package/dist/assets/src/execution/sandboxErrors.ts +9 -4
  25. package/dist/assets/web/dist/.vite/manifest.json +481 -462
  26. package/dist/assets/web/dist/assets/AppShell-B4EUiSwP.js +1 -0
  27. package/dist/assets/web/dist/assets/CostsPage-D8VjAtuH.js +2 -0
  28. package/dist/assets/web/dist/assets/DeliveryPage-DpQi5XUI.js +1 -0
  29. package/dist/assets/web/dist/assets/HomePage-CvKuIGqg.js +2 -0
  30. package/dist/assets/web/dist/assets/InputMenu-DmqAxkn4.js +1 -0
  31. package/dist/assets/web/dist/assets/NotFoundPage-DDogyiXA.js +1 -0
  32. package/dist/assets/web/dist/assets/PendingTurnRow-BoSLlR6j.js +1 -0
  33. package/dist/assets/web/dist/assets/ResidentDetailPage-CiBdbzkE.js +1 -0
  34. package/dist/assets/web/dist/assets/ResidentsIndexPage-DgtHtd9K.js +1 -0
  35. package/dist/assets/web/dist/assets/{RunFoldRow-BPMQxTcl.js → RunFoldRow-IF4xbz-3.js} +1 -1
  36. package/dist/assets/web/dist/assets/RunRoutePage-DHYa7rHK.js +9 -0
  37. package/dist/assets/web/dist/assets/RunsIndexPage-aWhNTv8r.js +1 -0
  38. package/dist/assets/web/dist/assets/RunsTabs-CY_tdGTU.js +1 -0
  39. package/dist/assets/web/dist/assets/ScheduledPage-gPEBZjcx.js +1 -0
  40. package/dist/assets/web/dist/assets/SettingSelect-VDanrTZi.js +1 -0
  41. package/dist/assets/web/dist/assets/SettingsPage-CFoltSRD.js +1 -0
  42. package/dist/assets/web/dist/assets/StatusDot-DA3eTVrT.js +1 -0
  43. package/dist/assets/web/dist/assets/Tooltip-BgbCkM12.js +1 -0
  44. package/dist/assets/web/dist/assets/UnitRoutePage-DibegaJJ.js +1 -0
  45. package/dist/assets/web/dist/assets/{angular-html-Dail07rr.js → angular-html-D_3slZqf.js} +1 -1
  46. package/dist/assets/web/dist/assets/{angular-ts-BReExio8.js → angular-ts-D1Fv2cpN.js} +1 -1
  47. package/dist/assets/web/dist/assets/{apl-DhNO6Yyx.js → apl-60Nm_trN.js} +1 -1
  48. package/dist/assets/web/dist/assets/{astro-BYotTY4C.js → astro-CvyaM3Q9.js} +1 -1
  49. package/dist/assets/web/dist/assets/{blade-mUxTtD1X.js → blade-yan3VBls.js} +1 -1
  50. package/dist/assets/web/dist/assets/{c-DhuiwcSQ.js → c-Ddgepap2.js} +1 -1
  51. package/dist/assets/web/dist/assets/{chapel-GD2vFtRi.js → chapel-CgNY6dxC.js} +1 -1
  52. package/dist/assets/web/dist/assets/{cobol-BhFDwkes.js → cobol-C7q82cdP.js} +1 -1
  53. package/dist/assets/web/dist/assets/{coffee-DWUgbE0r.js → coffee-DgRMF_ni.js} +1 -1
  54. package/dist/assets/web/dist/assets/{cpp-CO9XFjGW.js → cpp-B8Lf-aeo.js} +1 -1
  55. package/dist/assets/web/dist/assets/{crystal-B9pVurpn.js → crystal-CkA_dhEO.js} +1 -1
  56. package/dist/assets/web/dist/assets/{css-CFVVV6lI.js → css-Cyu-C_oA.js} +1 -1
  57. package/dist/assets/web/dist/assets/{dist-BtPs2AkQ.js → dist-DS-gMjLZ.js} +2 -2
  58. package/dist/assets/web/dist/assets/{durationTone-BobbycC-.js → durationTone-l6aVrHb8.js} +1 -1
  59. package/dist/assets/web/dist/assets/{edge-BDhIDQIb.js → edge-BxV3rKNI.js} +1 -1
  60. package/dist/assets/web/dist/assets/{elixir-DG3J8WoD.js → elixir-pNSEK39d.js} +1 -1
  61. package/dist/assets/web/dist/assets/{elm-FiOdiaOE.js → elm-zgwWE33W.js} +1 -1
  62. package/dist/assets/web/dist/assets/{erb-vxbtW503.js → erb-yzcYAHp_.js} +1 -1
  63. package/dist/assets/web/dist/assets/{eventSource-tRprhqm7.js → eventSource-Ce94ho6M.js} +1 -1
  64. package/dist/assets/web/dist/assets/{git-rebase-DINz8M3H.js → git-rebase-Dg7Ous1-.js} +1 -1
  65. package/dist/assets/web/dist/assets/{glimmer-js-yann6r9i.js → glimmer-js-CbZEwX27.js} +1 -1
  66. package/dist/assets/web/dist/assets/{glimmer-ts-CXTbrZn_.js → glimmer-ts-D3NIqALy.js} +1 -1
  67. package/dist/assets/web/dist/assets/{glsl-DNYC-hq0.js → glsl-y4-3WrI2.js} +1 -1
  68. package/dist/assets/web/dist/assets/{graphql-CwzhHzi9.js → graphql-D5d0al7K.js} +1 -1
  69. package/dist/assets/web/dist/assets/{hack-BBXF1cwR.js → hack-D8fD6kEl.js} +1 -1
  70. package/dist/assets/web/dist/assets/{haml-oEqBcsTy.js → haml-BWeFFc-V.js} +1 -1
  71. package/dist/assets/web/dist/assets/{handlebars-YE5thkT1.js → handlebars-gIm5FeE_.js} +1 -1
  72. package/dist/assets/web/dist/assets/{html-derivative-DCT1GYtQ.js → html-derivative-B3ySSBrQ.js} +1 -1
  73. package/dist/assets/web/dist/assets/{html-Dp4jRQ1j.js → html-fXJTVLSM.js} +1 -1
  74. package/dist/assets/web/dist/assets/{http-BPcGbmzJ.js → http-DrJfQUmw.js} +1 -1
  75. package/dist/assets/web/dist/assets/{hurl-CQnNHwyd.js → hurl-Bvbnsf55.js} +1 -1
  76. package/dist/assets/web/dist/assets/{java-Dm8Ovj6W.js → java-CUSpb-i-.js} +1 -1
  77. package/dist/assets/web/dist/assets/{javascript-Btm9oJK9.js → javascript-BWRH5fbY.js} +1 -1
  78. package/dist/assets/web/dist/assets/{jinja-VZYjWF_U.js → jinja-D3Ymci8y.js} +1 -1
  79. package/dist/assets/web/dist/assets/{jison-Dcmjxa9x.js → jison-Gt9SO7ZC.js} +1 -1
  80. package/dist/assets/web/dist/assets/{json-DoWiOGZ8.js → json-Cfcblhgn.js} +1 -1
  81. package/dist/assets/web/dist/assets/{jsx-Dvmo55ma.js → jsx-C0366fFM.js} +1 -1
  82. package/dist/assets/web/dist/assets/{julia-d8AtfV26.js → julia-CyBOTbxS.js} +1 -1
  83. package/dist/assets/web/dist/assets/{just-Cc5EStA-.js → just-FPXZcIOd.js} +1 -1
  84. package/dist/assets/web/dist/assets/{latex-B5fKKIH7.js → latex-Bd-c88v0.js} +1 -1
  85. package/dist/assets/web/dist/assets/{liquid-DUYI9JND.js → liquid-5tu9Hc0k.js} +1 -1
  86. package/dist/assets/web/dist/assets/{lua-BhMwKWpw.js → lua-BZ5fdPP8.js} +1 -1
  87. package/dist/assets/web/dist/assets/main-B4A6x82d.js +28 -0
  88. package/dist/assets/web/dist/assets/main-CpkwzHX6.css +1 -0
  89. package/dist/assets/web/dist/assets/{marko-Cnf2cPAi.js → marko-C_mmj4p0.js} +1 -1
  90. package/dist/assets/web/dist/assets/{mdc-FDDXoir9.js → mdc-CtaNqWjY.js} +1 -1
  91. package/dist/assets/web/dist/assets/{nginx-q-8U_1jR.js → nginx-Ddm5Mb8J.js} +1 -1
  92. package/dist/assets/web/dist/assets/{nim-DIA0TUWs.js → nim-COhfroYB.js} +1 -1
  93. package/dist/assets/web/dist/assets/{org-CR88hy7S.js → org-CCHFgmTb.js} +1 -1
  94. package/dist/assets/web/dist/assets/{perl-Bkd8N7ja.js → perl-CXzOE-Z-.js} +1 -1
  95. package/dist/assets/web/dist/assets/{php-BOs84mvy.js → php-1V6jAIhU.js} +1 -1
  96. package/dist/assets/web/dist/assets/{pug-BbpZBb3w.js → pug-uS37efts.js} +1 -1
  97. package/dist/assets/web/dist/assets/{qml-D2jQpjFE.js → qml-Bj1A0Ben.js} +1 -1
  98. package/dist/assets/web/dist/assets/{r-DKzcAQ22.js → r-CMbKbLsS.js} +1 -1
  99. package/dist/assets/web/dist/assets/{razor-SOQMWcC9.js → razor-DVRq_Ndk.js} +1 -1
  100. package/dist/assets/web/dist/assets/{regexp-B6KK4W8d.js → regexp-AbaOr4oB.js} +1 -1
  101. package/dist/assets/web/dist/assets/{rst-kPSg-NPf.js → rst-Dq_QWgOF.js} +1 -1
  102. package/dist/assets/web/dist/assets/{ruby-cKS-JYLH.js → ruby-CVSsAj7T.js} +1 -1
  103. package/dist/assets/web/dist/assets/{sas-DNxzE5gs.js → sas-C-dGfxUi.js} +1 -1
  104. package/dist/assets/web/dist/assets/{scss-DGcHQ5OO.js → scss-DxALxYPA.js} +1 -1
  105. package/dist/assets/web/dist/assets/seed-BhxEVasY.js +6 -0
  106. package/dist/assets/web/dist/assets/{settingsApi-priOPiwu.js → settingsApi-Dm6eaQcu.js} +1 -1
  107. package/dist/assets/web/dist/assets/{shellscript-CRZeZBa5.js → shellscript--qXcvQ5L.js} +1 -1
  108. package/dist/assets/web/dist/assets/{shellsession-DSNEL0HX.js → shellsession-DeqnW8Ux.js} +1 -1
  109. package/dist/assets/web/dist/assets/{soy-zisNbkDO.js → soy-DreoUIMt.js} +1 -1
  110. package/dist/assets/web/dist/assets/{sql-MG06KMMb.js → sql-o3i2VTG6.js} +1 -1
  111. package/dist/assets/web/dist/assets/{sseReplay-DaRtc7nI.js → sseReplay-qF-gpbAY.js} +3 -3
  112. package/dist/assets/web/dist/assets/{stata-CB_wKwv9.js → stata-CGAT41H7.js} +1 -1
  113. package/dist/assets/web/dist/assets/{surrealql-DYdSt7re.js → surrealql-CBU0QuGV.js} +1 -1
  114. package/dist/assets/web/dist/assets/{svelte-DHO-F7c7.js → svelte-CfHaLZiG.js} +1 -1
  115. package/dist/assets/web/dist/assets/{templ-CUL5L4Lr.js → templ-D02MUcma.js} +1 -1
  116. package/dist/assets/web/dist/assets/{tex-_JNEKjVW.js → tex-DjprHYXl.js} +1 -1
  117. package/dist/assets/web/dist/assets/{ts-tags--9EpEV_8.js → ts-tags-DftDOzP-.js} +1 -1
  118. package/dist/assets/web/dist/assets/{tsx-BcNq2x2u.js → tsx-BOdvPohJ.js} +1 -1
  119. package/dist/assets/web/dist/assets/{twig-EsY5Dk_D.js → twig-B_yLS56B.js} +1 -1
  120. package/dist/assets/web/dist/assets/{typescript-CyA_HiUH.js → typescript-CdPCa4nU.js} +1 -1
  121. package/dist/assets/web/dist/assets/{typst-SzaHlDt4.js → typst-C1oIFsUl.js} +1 -1
  122. package/dist/assets/web/dist/assets/{vue-D3cn5acV.js → vue-CcszJEX8.js} +1 -1
  123. package/dist/assets/web/dist/assets/{vue-html-6MxhUxOa.js → vue-html-DcpxPcuT.js} +1 -1
  124. package/dist/assets/web/dist/assets/{vue-vine-qJnqhgo9.js → vue-vine-D26bIKGj.js} +1 -1
  125. package/dist/assets/web/dist/assets/wallClock-DPTuP2FD.js +1 -0
  126. package/dist/assets/web/dist/assets/{xml-DyRpC7xv.js → xml-D8zl6W5P.js} +1 -1
  127. package/dist/assets/web/dist/assets/{xsl-BXMRPAsg.js → xsl-D86Bii28.js} +1 -1
  128. package/dist/assets/web/dist/assets/{yaml-BNmPCtAN.js → yaml-g6Px4KFU.js} +1 -1
  129. package/dist/cli.js +3023 -1049
  130. package/package.json +1 -1
  131. package/dist/assets/web/dist/assets/AppShell-Ck4hSZDi.js +0 -1
  132. package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +0 -2
  133. package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +0 -1
  134. package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +0 -2
  135. package/dist/assets/web/dist/assets/NotFoundPage-CiGrVOyv.js +0 -1
  136. package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +0 -1
  137. package/dist/assets/web/dist/assets/ResidentDetailPage-C5USV_8P.js +0 -1
  138. package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +0 -1
  139. package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +0 -9
  140. package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +0 -1
  141. package/dist/assets/web/dist/assets/RunsTabs-BWRHzhtT.js +0 -1
  142. package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +0 -1
  143. package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +0 -1
  144. package/dist/assets/web/dist/assets/StatusDot-_Q_bmReh.js +0 -1
  145. package/dist/assets/web/dist/assets/Tooltip-Cafsgd4m.js +0 -1
  146. package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +0 -1
  147. package/dist/assets/web/dist/assets/main-C4366PZF.css +0 -1
  148. package/dist/assets/web/dist/assets/main-DlTNAC6c.js +0 -28
  149. package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +0 -6
  150. package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +0 -1
@@ -81,7 +81,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
81
81
  import type { DirectoryBackup, SandboxCommand } from "@cloudflare/sandbox";
82
82
  import { createExtensionProcessSandbox } from "@cloudflare/sandbox/extensions";
83
83
  import { DurableObject } from "cloudflare:workers";
84
- import { BASH_TIMEOUT_MAX_MS, BASH_TIMEOUT_MS, clampBashTimeout } from "../../src/execution/bashTimeout.js";
84
+ import { BASH_TIMEOUT_MAX_MS, clampBashTimeout } from "../../src/execution/bashTimeout.js";
85
85
  import { selectBindingsToPurge } from "../../src/execution/bindingPurge.js";
86
86
  import { busyAfterKillReason, planForceDetach } from "../../src/execution/residentDetach.js";
87
87
  import { parseReadonly, planReadonlyAttach } from "../../src/execution/residentReadonly.js";
@@ -323,6 +323,16 @@ import {
323
323
  } from "../../src/execution/residentDepsStore.js";
324
324
  import { buildId, injectedBuildStamp } from "../../src/deploy/buildStamp.js";
325
325
  import { createRefreshInstance, createRefreshInstanceNow, type RefreshInstanceParams } from "./refresh";
326
+ import {
327
+ ControlResetError,
328
+ RuntimeReplacedError,
329
+ controlResetErr,
330
+ execFailureDocument,
331
+ runtimeReplacedErr,
332
+ selfAndCauses,
333
+ threadErrBuilders,
334
+ type ThreadErr,
335
+ } from "./threadErr";
326
336
  import {
327
337
  DEFAULT_EXEC_TIMEOUT_MS,
328
338
  DEPS_STEP_OVERHEAD_MS,
@@ -621,9 +631,11 @@ const ATTACH_MUTEX_WAIT_MS = 60_000;
621
631
  * stays far inside the bot's 45-min run budget. Work beyond 20 minutes
622
632
  * belongs in background jobs.
623
633
  *
624
- * /op runs (test/build) keep the flat 5-minute budget — the deterministic op
625
- * path has no caller-supplied knob, so nothing may stretch it. */
626
- const OP_EXEC_TIMEOUT_MS = BASH_TIMEOUT_MS;
634
+ * /op runs (test/build) get the flat 20-minute ceiling — the deterministic op
635
+ * path has no caller-supplied knob, and a repo's full suite routinely outlives
636
+ * the 5-minute per-command default; the client bounds its /op wait at the same
637
+ * ceiling, so the two ends agree. */
638
+ const OP_EXEC_TIMEOUT_MS = BASH_TIMEOUT_MAX_MS;
627
639
  /** Sanity bound on /exec's command body — a guard against a runaway caller,
628
640
  * not a working limit: legitimate agent one-liners (heredocs writing test
629
641
  * files, `node -e` scripts, long pipelines) run well past the old 8 000 and
@@ -674,70 +686,10 @@ export function validateEnvNames(vars: Record<string, string>): void {
674
686
  }
675
687
  }
676
688
 
677
- /** The resident runtime (the Sandbox SDK's control session to the container)
678
- * was replaced while a command was in flight — in practice a `wrangler deploy`
679
- * swapping this DO's isolate mid-run (which otherwise surfaces as a fake
680
- * "OOM" on the run). `phase` says where the SDK failed: `"spawn"`
681
- * (the start RPC itself; the SDK never proves the process did NOT start) or
682
- * `"collect"` (a `StaleProcessHandleError` on an already-running process). In
683
- * both cases the command may have run, so the resident never re-issues it;
684
- * the thread routes answer the NAMED `runtime-replaced` error and the client
685
- * decides (idempotent read/write retry; exec is handed to the model). */
686
- class RuntimeReplacedError extends Error {
687
- constructor(
688
- /** Where the replacement was met: the command's spawn, its collect, or
689
- * the Worker's call into the Durable Object itself (`threadRejectionErr`),
690
- * before the method could answer. */
691
- readonly phase: "spawn" | "collect" | "call",
692
- readonly cause: unknown,
693
- /** Whether the replacement was KNOWN where the failure was classified
694
- * (`replacementKnown`): the SDK vouched the runtime moved, or a restore is
695
- * under way for this resident. One decision, made once at the exec choke
696
- * point, and it gates the word on `/exec` alone (`replacedExecAnswer`) —
697
- * the incarnation swap is unconditional there. Unknown, the failure only
698
- * says the container is down or the transport was lost — a merely asleep
699
- * or starting container, or a network blip, say the same — so the answer
700
- * is the SDK's words and the harness's one more command decides. At the
701
- * Worker's call nothing vouched, so it is `false` there. */
702
- readonly known: boolean,
703
- ) {
704
- super(
705
- `runtime-replaced: the resident runtime was replaced (a deploy) while this command was ${
706
- phase === "spawn" ? "starting" : phase === "collect" ? "running" : "pending at the Worker"
707
- }; its output is lost (${errMsg(cause)})`,
708
- );
709
- this.name = "RuntimeReplacedError";
710
- }
711
- }
712
-
713
- /** The resident's own Durable Object was reset while a command was in flight —
714
- * a `wrangler deploy` of the Worker code (not the container image) supersedes
715
- * this DO's isolate, so the SDK's control session to the container is lost
716
- * mid-command (`isDurableObjectCodeUpdateReset`). This is NOT a runtime
717
- * replacement: the container and every process the run holds in it are exactly
718
- * as they were — only the DO that was driving them reset. The command's
719
- * outcome is unknown (the reset may have raced its start or its finish), so
720
- * the resident never re-issues it here; the thread routes answer the NAMED
721
- * `control-reset` error and the client re-sends an idempotent op or resolves a
722
- * write by pi's echo (docs/reference/specs/harness-pi.md item 16). Distinct
723
- * from `RuntimeReplacedError` precisely so the harness never mistakes a DO
724
- * reset over a live pi for a replaced container and orphans that pi. */
725
- class ControlResetError extends Error {
726
- constructor(
727
- /** Where the reset was met: the command's spawn, its collect, or the
728
- * Worker's call into the Durable Object itself (`threadRejectionErr`),
729
- * before the method could answer. */
730
- readonly phase: "spawn" | "collect" | "call",
731
- readonly cause: unknown,
732
- ) {
733
- super(
734
- `control-reset: the resident's Durable Object was reset (a deploy) while this command was ${
735
- phase === "spawn" ? "starting" : phase === "collect" ? "running" : "pending at the Worker"
736
- }; the container and its processes are as they were; the command's outcome is unknown (${errMsg(cause)})`,
737
- );
738
- this.name = "ControlResetError";
739
- }
740
- }
689
+ // `RuntimeReplacedError`, `ControlResetError`, the `ThreadErr` shape and the
690
+ // builders that answer a throw no route named live in ./threadErr.ts, pure
691
+ // over `(err, route)` so plain Node runs them; this entry hands them the
692
+ // predicates only it can supply (`threadErrBuilders`, below the predicates).
741
693
 
742
694
  /** Every wording the pinned SDK (@cloudflare/sandbox@0.13.0-next.751.1) uses
743
695
  * when the runtime incarnation changed under a call, for the message-based
@@ -755,17 +707,6 @@ class ControlResetError extends Error {
755
707
  // port-ready bound. Both take the message fallback below; neither has a typed
756
708
  // class to match.
757
709
 
758
- /** `err` and its `cause` chain, bounded like the SDK's own `selfAndCauses`
759
- * walker: the SDK wraps platform errors, so the telling message can sit one or
760
- * two links down. */
761
- function* selfAndCauses(err: unknown): Generator<unknown> {
762
- let current = err;
763
- for (let depth = 0; depth < 8 && current != null; depth++) {
764
- yield current;
765
- current = typeof current === "object" ? (current as { cause?: unknown }).cause : undefined;
766
- }
767
- }
768
-
769
710
  /** `OperationInterruptedError` reasons that mean the runtime under the call is
770
711
  * gone and a new one serves the thread: a deploy swapped the isolate
771
712
  * (`runtime_replaced`), the sandbox's lifetime epoch moved
@@ -848,107 +789,17 @@ function sdkVouchesRuntimeMoved(err: unknown): boolean {
848
789
  return false;
849
790
  }
850
791
 
851
- /** The platform's transient sentences the pinned SDK's own predicate does not
852
- * name — `isPlatformTransientError` covers the code-update reset, a lost
853
- * connection, the storage-startup reset and the typed `retryable` flag, so
854
- * those are read from the SDK, never from a string table of ours. Two remain:
855
- * the Durable Object reset by a storage operation that did not complete (the
856
- * platform's `Durable Object storage operation exceeded timeout which caused
857
- * object to be reset.` — an assumption about the platform's text, carried
858
- * from the first typing of the catch-all, not in the SDK's source), and the
859
- * Durable Object overloaded. The SDK's retry predicate EXCLUDES the overloaded
860
- * sentence (`isErrorRetryable`: an in-process retry only adds to a queue that
861
- * is full); it is typed transient here because the client's
862
- * `worker-unavailable` is a re-probe after the harness's bounded wait
863
- * (`replacedVerdict`, `PROBE_WAIT_MAX_MS`), never a tight retry, and a full
864
- * queue drains. The platform's sentences alone — a bare `internal error` or
865
- * `overloaded` is a word a git or GitHub failure carries too, and a throw
866
- * typed transient by it would be re-probed for a restore window that never
867
- * clears it. */
868
- const TRANSIENT_PLATFORM_WORDING = /storage operation|durable object is overloaded/i;
869
-
870
- /** The platform's own transient that no route word names — what is left once
871
- * `threadRejectionErr` has answered a reset and a replacement by their words:
872
- * the container's runtime unreachable (`isRuntimeUnreachable`: the SDK's
873
- * connect abort by its `AbortError` name or its exact sentence —
874
- * `RUNTIME_UNREACHABLE_WORDING` is anchored to the whole message, so a
875
- * `TimeoutError`'s `The operation was aborted due to timeout`, the text a
876
- * route's own `AbortSignal.timeout` on a slow GitHub raises, never matches),
877
- * the pinned SDK's own platform-transient predicate (`isPlatformTransientError`:
878
- * the code-update reset, a lost connection, the storage-startup reset, the
879
- * typed `retryable` flag — the SDK's signal, read as it reads it), and the
880
- * remainder sentences above, anywhere in the cause chain. */
881
- function isUnnamedPlatformTransient(err: unknown): boolean {
882
- if (isRuntimeUnreachable(err) || isPlatformTransientError(err)) return true;
883
- for (const link of selfAndCauses(err)) {
884
- if (link instanceof Error && TRANSIENT_PLATFORM_WORDING.test(link.message)) return true;
885
- }
886
- return false;
887
- }
888
-
889
- /** Whether a throw no route named is the platform's own transient — the
890
- * Durable Object reset by a deploy (`isControlReset`), the runtime replaced
891
- * under the call (`isRuntimeReplacement`), or the transient no word names
892
- * (`isUnnamedPlatformTransient`) — against a deterministic throw in the route
893
- * itself (a bug, a bad argument). The client reads the answer's `transient`
894
- * field to re-probe the first and judge the second at once (execution.md item
895
- * 9), never the words. */
896
- function isTransientPlatformThrow(err: unknown): boolean {
897
- return isControlReset(err) || isRuntimeReplacement(err) || isUnnamedPlatformTransient(err);
898
- }
899
-
900
- /** The 500 for a throw no route named, at whichever catch met it — the fetch
901
- * handler's catch-all, a streamed route's rejection mapper, a route's own
902
- * catch around its body (`prefix`: `attach-failed`, `op-failed`): the words,
903
- * and whether the throw was the platform's transient (`transient`), typed
904
- * here so the client decides by a field and never by which catch met the
905
- * throw. A failure a route DID name — a step that failed, a mirror held — is
906
- * that route's own answer and never comes here. */
907
- function catchAllErr(err: unknown, prefix?: string): ThreadErr {
908
- return unnamedThrowErr(err, isTransientPlatformThrow(err), prefix);
909
- }
910
-
911
- /** The one shape of that 500: the words, behind `prefix` where the route names
912
- * itself, and the verdict `transient`, decided once by the caller — so a
913
- * caller that has already settled the typed predicates (`threadRejectionErr`)
914
- * hands its verdict in instead of walking the cause chain again. */
915
- function unnamedThrowErr(err: unknown, transient: boolean, prefix?: string): ThreadErr {
916
- const words = errMsg(err);
917
- return { error: prefix ? `${prefix}: ${words}` : words, status: 500, transient };
918
- }
919
-
920
- type ThreadDataRoute = "/exec" | "/read" | "/write";
921
-
922
- /** A thread data-plane route's pending Durable Object call that REJECTED —
923
- * the stub, not the method: the DO reset by a code update before or while the
924
- * method ran, a storage operation that did not complete, the runtime
925
- * unreachable — answered as the DO answers the same fact when it catches it
926
- * inside (`execThreadImpl`, the file methods). A control reset is the DO's
927
- * own word, `control-reset` on a 409: the container and its processes are as
928
- * they were and the command's outcome is unknown, so the client resolves it by
929
- * its own rule and never waits on it as the resident unavailable. A runtime
930
- * replacement is answered by route, as the methods answer it, with the DO's
931
- * own gate (`replacementKnown`) applied as far as it reaches: its first half,
932
- * the SDK vouching the runtime moved (`sdkVouchesRuntimeMoved`), judges the
933
- * moved sentences that survive the stub boundary as text, so it is made here
934
- * by the same rule; its other half, a restore under way (`knowsContainerGone`),
935
- * is the DO's alone and out of reach. So on `/exec` the word is said where
936
- * the SDK vouched and WITHHELD where only the DO could have — that gate's
937
- * unknown branch, the SDK's words on a 409 with no word, for the harness's one
938
- * more command to judge; on `/read` and `/write` the word is said either way
939
- * (`runtimeReplacedErr`, unconditional there on purpose), since it drives the
940
- * client's one re-attach-and-retry and never a verdict. Anything else is the
941
- * typed 500, its `transient` decided here once the typed predicates are
942
- * settled. */
943
- function threadRejectionErr(err: unknown, route: ThreadDataRoute): ThreadErr {
944
- if (isControlReset(err)) return controlResetErr(new ControlResetError("call", err));
945
- if (isRuntimeReplacement(err)) {
946
- const known = sdkVouchesRuntimeMoved(err);
947
- if (route === "/exec" && !known) return { error: errMsg(err), status: 409 };
948
- return runtimeReplacedErr(new RuntimeReplacedError("call", err, known));
949
- }
950
- return unnamedThrowErr(err, isUnnamedPlatformTransient(err));
951
- }
792
+ /** The 500 for a throw no route named and the thread data plane's rejection
793
+ * mapping (threadErr.ts holds the rules; threadErr.test.ts runs them under
794
+ * plain Node with the SDK's real sentences), built over the predicates only
795
+ * this entry can supply: the SDK's reset and platform-transient predicates,
796
+ * and the resident's own replacement classifier and vouch. */
797
+ const { catchAllErr, threadRejectionErr } = threadErrBuilders({
798
+ isControlReset,
799
+ isRuntimeReplacement,
800
+ sdkVouchesRuntimeMoved,
801
+ isPlatformTransientError,
802
+ });
952
803
 
953
804
  /** Whether a command's failure is the resident's own Durable Object resetting
954
805
  * under it (a Worker-code deploy) rather than the container runtime being
@@ -960,31 +811,6 @@ function isControlReset(err: unknown): boolean {
960
811
  return isDurableObjectCodeUpdateReset(err);
961
812
  }
962
813
 
963
- /** The named ThreadErr every thread route (exec/read/write) answers for a
964
- * runtime replacement, so the client can classify it (409 like the other
965
- * recoverable thread states; `reason` is the discriminator). On `/exec` the
966
- * answer goes through `replacedExecAnswer` first: the word only when the
967
- * replacement was known where `run()` classified the failure. On
968
- * `/read` and `/write` the word stays unconditional on purpose: those routes
969
- * reach the same `run()` and can meet the same down wordings, but there the
970
- * word drives the client's re-attach-and-retry (the attach is what wakes a
971
- * container that is asleep or starting), never a verdict — nothing relaunches
972
- * on it, and the harness never reads or writes through them (its seam runs
973
- * every operation as an `/exec` script). Gating them would trade a spare
974
- * re-attach for a read that fails outright while the container starts. */
975
- function runtimeReplacedErr(err: RuntimeReplacedError): ThreadErr {
976
- return { error: err.message, status: 409, reason: "runtime-replaced" };
977
- }
978
-
979
- /** The named ThreadErr a DO code-update reset answers with — its own `reason`
980
- * the client keys on, distinct from `runtime-replaced`: the container is
981
- * unchanged and the command's outcome is unknown, so the client re-sends an
982
- * idempotent op or resolves a write by echo (harness-pi item 16), never the
983
- * replaced verdict. */
984
- function controlResetErr(err: ControlResetError): ThreadErr {
985
- return { error: err.message, status: 409, reason: "control-reset" };
986
- }
987
-
988
814
  /** The container's control port never answered: `exec` rejected with the
989
815
  * DOMException of the SDK's connect abort (`DEFAULT_CONNECT_TIMEOUT_MS`,
990
816
  * 30 s), raised inside its wake path — `RuntimeBootstrapProbe.probe` →
@@ -1308,36 +1134,6 @@ interface ThreadBinding {
1308
1134
  depsKey?: string;
1309
1135
  }
1310
1136
 
1311
- /** Named, RPC-cloneable error shape for the thread data plane. The Worker
1312
- * maps `status` to the HTTP status; extra fields (`needs`, `state`,
1313
- * `reason`) ride along into the body. */
1314
- interface ThreadErr {
1315
- error: string;
1316
- status: number;
1317
- /** Beside `state` on a 503 that carries the lifecycle state: the lifecycle
1318
- * REASON (`getStatus().reason`), kept apart from `reason`, which is the
1319
- * answer's own word (`mirror-busy`, `disk-pressure`, `image-stale`). The
1320
- * client reads the pair to decide whether the resident is coming back
1321
- * (execution.md item 9); reading `reason` there would take a busy mirror on
1322
- * a degraded-but-serviceable resident for a repo failure. */
1323
- stateReason?: string;
1324
- /** On the 500 for a throw no route named (`unnamedThrowErr`, through
1325
- * `catchAllErr` or `threadRejectionErr`: the fetch handler's catch-all, a
1326
- * streamed route's rejection, a route's own catch around its body): whether
1327
- * the throw it wrapped was the platform's own transient
1328
- * (`isTransientPlatformThrow`) — a re-probe clears it — or a deterministic
1329
- * throw in the route, judged at once. */
1330
- transient?: boolean;
1331
- /** The steps the request ran before it failed (docs/reference/specs/tracing.md item 19):
1332
- * a failed attach's trace is the one that says which step blew the budget. */
1333
- trace?: ResidentStep[];
1334
- needs?: string;
1335
- /** With needs:"ref" — the resident's default branch, so the caller can bind by default. */
1336
- defaultRef?: string;
1337
- state?: ResidentState;
1338
- reason?: string;
1339
- }
1340
-
1341
1137
  interface AttachOk {
1342
1138
  workspace: string;
1343
1139
  ref: string;
@@ -8990,33 +8786,6 @@ function streamHeartbeatJson<T>(
8990
8786
  return new Response(stream, { headers: { "content-type": "application/json" } });
8991
8787
  }
8992
8788
 
8993
- /** /exec's failure document, in the item-3 dual shape (`error` beside
8994
- * `stdout: ""`, `stderr`, `exitCode: 127`) so old and new executors both
8995
- * render it. The ThreadErr's fields ride beside the words, as the JSON routes
8996
- * carry them: `needs`; the lifecycle pair (`state`, `stateReason`); the
8997
- * answer's own word (`reason`, kept independent of `state` — `runtimeReplacedErr()`
8998
- * sets `reason: "runtime-replaced"` with NO state, and the client's
8999
- * deploy-vs-dead-transport check reads it); the answer's `status`; the
9000
- * catch-all's `transient`. The client types a streamed failure by these fields
9001
- * (execution.md item 9), and a document that dropped them would make every
9002
- * /exec failure a deterministic answer over HTTP 200. One builder for both of
9003
- * the stream's paths: a failure the Durable Object named, and a pending
9004
- * result that rejected. */
9005
- function execFailureDocument(failure: ThreadErr): object {
9006
- return {
9007
- error: failure.error,
9008
- ...(failure.needs ? { needs: failure.needs } : {}),
9009
- ...(failure.state ? { state: failure.state } : {}),
9010
- ...(typeof failure.stateReason === "string" ? { stateReason: failure.stateReason } : {}),
9011
- ...(failure.reason ? { reason: failure.reason } : {}),
9012
- status: failure.status,
9013
- ...(typeof failure.transient === "boolean" ? { transient: failure.transient } : {}),
9014
- stdout: "",
9015
- stderr: failure.error,
9016
- exitCode: 127,
9017
- };
9018
- }
9019
-
9020
8789
  /** /exec's payload mapping: a result as {stdout, stderr, exitCode, truncated};
9021
8790
  * a failure as `execFailureDocument` — the one the Durable Object named, or
9022
8791
  * the pending result's rejection answered as the DO would have
@@ -9137,19 +8906,18 @@ async function handleOp(env: Env, body: Record<string, unknown>, traceparent?: s
9137
8906
  return streamOp(stub.runOp(op as "test" | "build", ref, traceparent));
9138
8907
  }
9139
8908
 
9140
- /** /op's payload mapping: results pass through; a named error sheds its
9141
- * transport-only `status` field (the body is the contract, never the code); a
9142
- * pending op that rejected is the typed 500 (`catchAllErr`, so `transient`
9143
- * rides beside the words), its status shed the same way. */
8909
+ /** /op's payload mapping: results pass through; a failure streams as the typed
8910
+ * document it is — the named error with its `status` IN the body, a pending
8911
+ * op that rejected as the typed 500 (`catchAllErr`, so `transient` rides
8912
+ * beside the words) — exactly as `/exec`, `/attach` and `/await-restore`
8913
+ * stream theirs, so the client reads the status off the document by the one
8914
+ * rule (`answeredStatus`) and judges `transient` by the one rule too
8915
+ * (`isTransientRefusal`: a 5xx carrying it), never by the field on a 200. */
9144
8916
  function streamOp(pending: Promise<Awaited<ReturnType<ResidentDO["runOp"]>>>): Response {
9145
- const shedStatus = (failure: ThreadErr): object => {
9146
- const { status: _status, ...rest } = failure;
9147
- return rest;
9148
- };
9149
8917
  return streamHeartbeatJson(
9150
8918
  pending,
9151
- (result) => ("error" in result ? shedStatus(result) : result),
9152
- (err) => shedStatus(catchAllErr(err, "op-failed")),
8919
+ (result) => result,
8920
+ (err) => catchAllErr(err, "op-failed"),
9153
8921
  );
9154
8922
  }
9155
8923
 
@@ -49,7 +49,11 @@ import {
49
49
  import { envFromRequest } from "../../src/execution/sandboxEnv.js";
50
50
  import { IdleGuard, type IdleGuardHost } from "../../src/execution/sandboxIdle.js";
51
51
  import { StartGate, type StartGateHost, type StartingCause } from "../../src/execution/sandboxStart.js";
52
- import { RUNTIME_REPLACEMENT_WORDING, isRuntimeUnreachableSignal } from "../../src/execution/residentRefresh.js";
52
+ import {
53
+ RUNTIME_REPLACEMENT_WORDING,
54
+ isRuntimeUnreachableSignal,
55
+ selfAndCauses,
56
+ } from "../../src/execution/residentRefresh.js";
53
57
  import {
54
58
  fleetBusyAnswer,
55
59
  fleetBusyExecAnswer,
@@ -107,6 +111,13 @@ const SDK_PIN: string = sandboxPkg.dependencies["@cloudflare/sandbox"];
107
111
 
108
112
  const WORKDIR = "/workspace";
109
113
 
114
+ /** How long the SDK waits for the platform to grant a container instance
115
+ * before its start fails (docs/reference/specs/execution.md item 23). The
116
+ * SDK's default is 30 s and it retries, so a burst that the platform would
117
+ * not admit took 63 s to say so; at 10 s (the SDK allows 5 s to 300 s) the
118
+ * refusal reaches the start gate — and the executor's wait — in seconds. */
119
+ const INSTANCE_GET_TIMEOUT_MS = 10_000;
120
+
110
121
  // Default per-command time limit, enforced by coreutils `timeout` inside the
111
122
  // sandbox. The tuned 280s applies when the body carries no timeoutMs (an older
112
123
  // bot); a caller-supplied timeoutMs is clamped server-side to the shared
@@ -149,15 +160,6 @@ const RUNTIME_REPLACED_REASONS = new Set([
149
160
  * closed (a runtime crash or stop), the socket failed, the upgrade failed. */
150
161
  const RPC_TRANSPORT_LOSS_KINDS = new Set(["peer_closed", "connection_failed", "upgrade_failed", "session_disposed"]);
151
162
 
152
- /** `err` and its `cause` chain, bounded like the SDK's own walk. */
153
- function* selfAndCauses(err: unknown): Generator<unknown> {
154
- let link: unknown = err;
155
- for (let depth = 0; link !== null && link !== undefined && depth < 8; depth++) {
156
- yield link;
157
- link = typeof link === "object" ? (link as { cause?: unknown }).cause : undefined;
158
- }
159
- }
160
-
161
163
  /** Did the container's runtime change under the command? Typed first (the
162
164
  * Durable Object sees the SDK's own classes, so `instanceof` holds here), the
163
165
  * SDK's replacement wording second — the same list the resident Worker
@@ -811,7 +813,16 @@ export default {
811
813
  // One sandbox per thread; the DO name is the thread key. The stub's own
812
814
  // methods (`runCommand`, `readText`, …) are what the routes call — the
813
815
  // work happens in the Durable Object, the data comes back over RPC.
814
- const sandbox = getSandbox(env.Sandbox, threadKey);
816
+ const sandbox = getSandbox(env.Sandbox, threadKey, {
817
+ containerTimeouts: {
818
+ // A refused instance grant is learned in seconds, not after the SDK's
819
+ // 30 s default and its retries (63 s to "no container instance" under
820
+ // a burst, docs/reference/specs/execution.md item 23): the gate turns
821
+ // it into the wait token and the executor re-sends. A grant that is
822
+ // merely slow is re-asked on the next send, which finds it made.
823
+ instanceGetTimeoutMS: INSTANCE_GET_TIMEOUT_MS,
824
+ },
825
+ });
815
826
 
816
827
  const url = new URL(request.url);
817
828
  const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
@@ -16,6 +16,18 @@
16
16
  "workers": ["bot"],
17
17
  "note": "Anthropic Console key the bot runs on. Third-party."
18
18
  },
19
+ {
20
+ "name": "OPENAI_API_KEY",
21
+ "workers": ["bot"],
22
+ "optional": true,
23
+ "note": "OpenAI key for the example config's `openai` provider block (`apiKeyEnv: OPENAI_API_KEY`). Third-party. Optional: read at the first model call on that block, never at load; without it every other block runs and only a run whose model names `openai/…` fails, by this name."
24
+ },
25
+ {
26
+ "name": "OPENROUTER_API_KEY",
27
+ "workers": ["bot"],
28
+ "optional": true,
29
+ "note": "OpenRouter key for the example config's `openrouter` provider block (`apiKeyEnv: OPENROUTER_API_KEY`). Third-party. Optional: read at the first model call on that block, never at load; without it every other block runs and only a run whose model names `openrouter/…` fails, by this name."
30
+ },
19
31
  {
20
32
  "name": "ANTHROPIC_ADMIN_KEY",
21
33
  "workers": ["bot"],
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.244.0",
3
+ "version": "1.246.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.244.0",
9
+ "version": "1.246.0",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -20445,7 +20445,7 @@
20445
20445
  },
20446
20446
  "packages/switchboard": {
20447
20447
  "name": "@coreplane/switchboard",
20448
- "version": "1.244.0",
20448
+ "version": "1.246.0",
20449
20449
  "license": "Apache-2.0",
20450
20450
  "dependencies": {
20451
20451
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.244.0",
3
+ "version": "1.246.0",
4
4
  "private": true,
5
5
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.244.0",
3
- "commit": "de7fddf53659568639ad877e935a8b9c0177debd",
4
- "builtAt": "2026-09-17T15:03:14.350Z"
2
+ "version": "1.246.0",
3
+ "commit": "62e4e9ad464820900b07bed176e140af6682c598",
4
+ "builtAt": "2026-09-18T01:06:13.947Z"
5
5
  }