@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.
- package/dist/assets/config/config.example.yaml +20 -1
- package/dist/assets/deploy/cloudflare-memory/worker.ts +234 -29
- package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +483 -75
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +3 -3
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +31 -35
- package/dist/assets/src/config/profile.ts +68 -3
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/types.ts +10 -0
- package/dist/assets/src/core/budgets.ts +349 -0
- package/dist/assets/src/core/coordinator/contract.ts +32 -0
- package/dist/assets/src/core/coordinator/driver.ts +75 -22
- package/dist/assets/src/core/costs.ts +9 -76
- package/dist/assets/src/core/delivery.ts +1 -1
- package/dist/assets/src/core/modelPricing.ts +212 -0
- package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
- package/dist/assets/src/core/reviewVerdict.ts +7 -0
- package/dist/assets/src/core/runEvents.ts +72 -5
- package/dist/assets/src/core/runFriction.ts +3 -1
- package/dist/assets/src/core/runRecord.ts +78 -0
- package/dist/assets/src/core/runUsage.ts +158 -47
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +2 -1
- package/dist/assets/src/core/ship/coordinator.ts +312 -97
- package/dist/assets/src/core/ship/handoff.ts +9 -0
- package/dist/assets/src/core/ship/renewal.ts +155 -0
- package/dist/assets/src/core/trace/attrs.ts +11 -0
- package/dist/assets/src/core/trace/types.ts +4 -3
- package/dist/assets/src/execution/bashTimeout.ts +8 -5
- package/dist/assets/src/execution/residentRefresh.ts +52 -7
- package/dist/assets/src/execution/residentSteps.ts +1 -1
- package/dist/assets/src/execution/residentText.ts +8 -3
- package/dist/assets/src/mcp/registry.ts +3 -0
- package/dist/assets/web/dist/.vite/manifest.json +432 -429
- package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DTMBgnIW.js → ResidentDetailPage-C5USV_8P.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-ELoXHlFt.js → StatusDot-_Q_bmReh.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BoeFwYP2.js → Tooltip-Cafsgd4m.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
- package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BU5UivXC.js → dist-BtPs2AkQ.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
- package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
- package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +1 -0
- package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
- package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
- package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
- package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
- package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
- package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
- package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
- package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
- package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-g7ml86LM.js → sseReplay-DaRtc7nI.js} +5 -5
- package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
- package/dist/cli.js +5843 -2246
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CtmKhhOF.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CaDvXhzJ.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DXD2gLJk.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-B2fX10aW.css +0 -1
- package/dist/assets/web/dist/assets/main-D4EA1g6n.js +0 -28
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
// (docs/decisions/0009-residents-second-credential-domain.md).
|
|
70
70
|
import {
|
|
71
71
|
isDurableObjectCodeUpdateReset,
|
|
72
|
+
isPlatformTransientError,
|
|
72
73
|
OperationInterruptedError,
|
|
73
74
|
ProcessWaitTimeoutError,
|
|
74
75
|
RPCTransportError,
|
|
@@ -179,6 +180,7 @@ import {
|
|
|
179
180
|
isRuntimeUnreachableSignal,
|
|
180
181
|
killStaleBuildProcessesCommand,
|
|
181
182
|
planRefresh,
|
|
183
|
+
RUNTIME_MOVED_WORDING,
|
|
182
184
|
RUNTIME_REPLACEMENT_WORDING,
|
|
183
185
|
RUNTIME_UNREACHABLE_DOWN_AT,
|
|
184
186
|
runtimeUnreachableReason,
|
|
@@ -195,6 +197,7 @@ import {
|
|
|
195
197
|
type RefreshPlan,
|
|
196
198
|
type RestoreSample,
|
|
197
199
|
type RefreshFailure,
|
|
200
|
+
fetchFailureIsMirrors,
|
|
198
201
|
} from "../../src/execution/residentRefresh.js";
|
|
199
202
|
import { autoRebuildDecision, isAutoRebuildEligible } from "../../src/execution/residentAutoRebuild.js";
|
|
200
203
|
import {
|
|
@@ -682,18 +685,60 @@ export function validateEnvNames(vars: Record<string, string>): void {
|
|
|
682
685
|
* decides (idempotent read/write retry; exec is handed to the model). */
|
|
683
686
|
class RuntimeReplacedError extends Error {
|
|
684
687
|
constructor(
|
|
685
|
-
|
|
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",
|
|
686
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,
|
|
687
703
|
) {
|
|
688
704
|
super(
|
|
689
705
|
`runtime-replaced: the resident runtime was replaced (a deploy) while this command was ${
|
|
690
|
-
phase === "spawn" ? "starting" : "running"
|
|
706
|
+
phase === "spawn" ? "starting" : phase === "collect" ? "running" : "pending at the Worker"
|
|
691
707
|
}; its output is lost (${errMsg(cause)})`,
|
|
692
708
|
);
|
|
693
709
|
this.name = "RuntimeReplacedError";
|
|
694
710
|
}
|
|
695
711
|
}
|
|
696
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
|
+
}
|
|
741
|
+
|
|
697
742
|
/** Every wording the pinned SDK (@cloudflare/sandbox@0.13.0-next.751.1) uses
|
|
698
743
|
* when the runtime incarnation changed under a call, for the message-based
|
|
699
744
|
* fallback below. Two of these come from classes the SDK does NOT export
|
|
@@ -752,27 +797,194 @@ const RPC_TRANSPORT_LOSS_KINDS = new Set(["peer_closed", "connection_failed", "u
|
|
|
752
797
|
/** Does this SDK error mean the runtime incarnation changed under us? Typed
|
|
753
798
|
* checks first (`StaleProcessHandleError`, `RuntimeIdentityInactiveError`, an
|
|
754
799
|
* `OperationInterruptedError` with one of `RUNTIME_REPLACED_REASONS`, an
|
|
755
|
-
* `RPCTransportError` with one of `RPC_TRANSPORT_LOSS_KINDS
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
800
|
+
* `RPCTransportError` with one of `RPC_TRANSPORT_LOSS_KINDS`), then the SDK's
|
|
801
|
+
* message wording — on the error AND its cause chain — as a belt-and-braces
|
|
802
|
+
* fallback. Anything else (a real spawn failure, a timeout, a wire-format
|
|
803
|
+
* error) is NOT a runtime replacement.
|
|
804
|
+
*
|
|
805
|
+
* Deliberately IMMUNE to the platform's superseded-isolate reset (a DO
|
|
806
|
+
* code-update reset): that is this Durable Object resetting over a container it
|
|
807
|
+
* did not touch, the container unchanged, so it is never a runtime replacement
|
|
808
|
+
* and never swaps the incarnation on the `/exec` path. `run()` answers it as a
|
|
809
|
+
* `control-reset` (`isControlReset`), and the restore path folds it in
|
|
810
|
+
* explicitly at its own site (`isRuntimeReplacement(err) || isControlReset(err)`)
|
|
811
|
+
* — the ONE place a DO reset shares the replacement disposition, since a reset
|
|
812
|
+
* under a live restore leaves nothing to land and re-restores onto the container
|
|
813
|
+
* that comes back. Keeping it out of this predicate makes that use the only one. */
|
|
759
814
|
function isRuntimeReplacement(err: unknown): boolean {
|
|
760
815
|
if (err instanceof StaleProcessHandleError) return true;
|
|
761
816
|
if (err instanceof RuntimeIdentityInactiveError) return true;
|
|
762
817
|
if (err instanceof OperationInterruptedError && RUNTIME_REPLACED_REASONS.has(err.reason)) return true;
|
|
763
818
|
if (err instanceof RPCTransportError && RPC_TRANSPORT_LOSS_KINDS.has(err.kind)) return true;
|
|
764
|
-
if (isDurableObjectCodeUpdateReset(err)) return true;
|
|
765
819
|
for (const link of selfAndCauses(err)) if (RUNTIME_REPLACEMENT_WORDING.test(errMsg(link))) return true;
|
|
766
820
|
return false;
|
|
767
821
|
}
|
|
768
822
|
|
|
823
|
+
/** Whether the SDK's failure vouches that the runtime under the command MOVED
|
|
824
|
+
* — a new incarnation serves the thread, so the container the command was
|
|
825
|
+
* aimed at is gone whatever else the resident knows: the typed classes for
|
|
826
|
+
* a stale process handle and an inactive runtime identity, an interruption
|
|
827
|
+
* whose reason is a replaced runtime, and the SDK's wordings for the same
|
|
828
|
+
* (`RUNTIME_MOVED_WORDING`, on the error and its cause chain). NOT the words
|
|
829
|
+
* for a container that is merely down (`STOPPED_CONTAINER_WORDING`: "The
|
|
830
|
+
* container is not running", "Process supervisor is closed") and NOT a
|
|
831
|
+
* transport lost under the call (`RPCTransportError`): an asleep or starting
|
|
832
|
+
* container and a network blip answer those too, so on `/exec` they are the
|
|
833
|
+
* word only when the resident itself knows the container it held is gone
|
|
834
|
+
* (`replacedExecAnswer`). NOT the platform's superseded-isolate reset either:
|
|
835
|
+
* that is this Durable Object resetting over a container it did not touch —
|
|
836
|
+
* `run()` intercepts it first, in both phases, as a `control-reset`
|
|
837
|
+
* (`isControlReset`), so a DO-reset cause never reaches this predicate, and a
|
|
838
|
+
* vouch for it here would say the opposite of that word. The restore path
|
|
839
|
+
* keeps the union by naming both explicitly at its site
|
|
840
|
+
* (`isRuntimeReplacement(err) || isControlReset(err)`), so a step interrupted
|
|
841
|
+
* either way is retried onto the container that comes back — while this
|
|
842
|
+
* predicate and `isRuntimeReplacement` stay immune to the reset. */
|
|
843
|
+
function sdkVouchesRuntimeMoved(err: unknown): boolean {
|
|
844
|
+
if (err instanceof StaleProcessHandleError) return true;
|
|
845
|
+
if (err instanceof RuntimeIdentityInactiveError) return true;
|
|
846
|
+
if (err instanceof OperationInterruptedError && RUNTIME_REPLACED_REASONS.has(err.reason)) return true;
|
|
847
|
+
for (const link of selfAndCauses(err)) if (RUNTIME_MOVED_WORDING.test(errMsg(link))) return true;
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
|
|
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
|
+
}
|
|
952
|
+
|
|
953
|
+
/** Whether a command's failure is the resident's own Durable Object resetting
|
|
954
|
+
* under it (a Worker-code deploy) rather than the container runtime being
|
|
955
|
+
* replaced. Checked BEFORE `isRuntimeReplacement` on the thread `run()` path,
|
|
956
|
+
* so a DO reset over a still-running container answers `control-reset`, never
|
|
957
|
+
* the replaced word. The SDK's predicate already walks the cause
|
|
958
|
+
* chain (`selfAndCauses`), so it is called directly on the caught error. */
|
|
959
|
+
function isControlReset(err: unknown): boolean {
|
|
960
|
+
return isDurableObjectCodeUpdateReset(err);
|
|
961
|
+
}
|
|
962
|
+
|
|
769
963
|
/** The named ThreadErr every thread route (exec/read/write) answers for a
|
|
770
964
|
* runtime replacement, so the client can classify it (409 like the other
|
|
771
|
-
* recoverable thread states; `reason` is the discriminator).
|
|
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. */
|
|
772
975
|
function runtimeReplacedErr(err: RuntimeReplacedError): ThreadErr {
|
|
773
976
|
return { error: err.message, status: 409, reason: "runtime-replaced" };
|
|
774
977
|
}
|
|
775
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
|
+
|
|
776
988
|
/** The container's control port never answered: `exec` rejected with the
|
|
777
989
|
* DOMException of the SDK's connect abort (`DEFAULT_CONNECT_TIMEOUT_MS`,
|
|
778
990
|
* 30 s), raised inside its wake path — `RuntimeBootstrapProbe.probe` →
|
|
@@ -1102,6 +1314,20 @@ interface ThreadBinding {
|
|
|
1102
1314
|
interface ThreadErr {
|
|
1103
1315
|
error: string;
|
|
1104
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;
|
|
1105
1331
|
/** The steps the request ran before it failed (docs/reference/specs/tracing.md item 19):
|
|
1106
1332
|
* a failed attach's trace is the one that says which step blew the budget. */
|
|
1107
1333
|
trace?: ResidentStep[];
|
|
@@ -1592,6 +1818,52 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
1592
1818
|
* survives, so hydration state is always probed from disk. */
|
|
1593
1819
|
private hydration: Promise<void> | null = null;
|
|
1594
1820
|
|
|
1821
|
+
/** Whether this resident knows the container it held is gone
|
|
1822
|
+
* (docs/reference/specs/resident-repos.md item 43): a restore under way for
|
|
1823
|
+
* this resident (`restoring`, persisted before any restore work) — its own
|
|
1824
|
+
* knowledge, reliable — and nothing else.
|
|
1825
|
+
*
|
|
1826
|
+
* Not the Container base's exit state, not a replayed `onStop`, not the
|
|
1827
|
+
* platform's `running` flag: each was tried or weighed and separates
|
|
1828
|
+
* nothing — a roll and an idle sleep leave the same traces, in both
|
|
1829
|
+
* directions — and the why, with the pinned library's lines, is the
|
|
1830
|
+
* record's alone (docs/reference/specs/resident-repos.md item 43), so a
|
|
1831
|
+
* dependency bump rots one place, not three. The harness's own probe
|
|
1832
|
+
* (`identity`, `alive`) stays the arbiter, so under-saying the word here is
|
|
1833
|
+
* safe and over-saying it is what orphans a process.
|
|
1834
|
+
*
|
|
1835
|
+
* A read that fails (the storage under a reset or a restore) is no
|
|
1836
|
+
* knowledge: the answer falls back to the SDK's words and the harness
|
|
1837
|
+
* probes, never an unhandled throw where a 409 was due. */
|
|
1838
|
+
private async knowsContainerGone(): Promise<boolean> {
|
|
1839
|
+
try {
|
|
1840
|
+
return (await this.getStatus()).state === "restoring";
|
|
1841
|
+
} catch {
|
|
1842
|
+
return false;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/** The one decision on a failure the SDK classified as a runtime
|
|
1847
|
+
* replacement, made where it is classified (`run()`): the SDK vouched the
|
|
1848
|
+
* runtime moved, or a restore is under way for this resident. It gates the
|
|
1849
|
+
* word on `/exec` and the incarnation mint alike; the per-incarnation memos
|
|
1850
|
+
* clear before it, unconditionally. */
|
|
1851
|
+
private async replacementKnown(err: unknown): Promise<boolean> {
|
|
1852
|
+
return sdkVouchesRuntimeMoved(err) || (await this.knowsContainerGone());
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
/** The `/exec` answer for a command the SDK failed with a runtime
|
|
1856
|
+
* replacement (item 43; harness.md item 6): the word — `reason:
|
|
1857
|
+
* "runtime-replaced"`, the resident's sentence — when the replacement was
|
|
1858
|
+
* known at the choke point (`RuntimeReplacedError.known`); otherwise what
|
|
1859
|
+
* the SDK said, with no word and no `reason`, so the harness's one more
|
|
1860
|
+
* command decides. The command is never re-issued either way: it may have
|
|
1861
|
+
* started. */
|
|
1862
|
+
private replacedExecAnswer(err: RuntimeReplacedError): ThreadErr {
|
|
1863
|
+
if (err.known) return runtimeReplacedErr(err);
|
|
1864
|
+
return { error: errMsg(err.cause), status: 409 };
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1595
1867
|
/** The step trace of the request in flight (docs/reference/specs/tracing.md item 19):
|
|
1596
1868
|
* `attachThread` and `runOp` each run inside their own collector, so the
|
|
1597
1869
|
* commands `runOk` runs and the mirror-lock waits land on the answer that
|
|
@@ -1952,6 +2224,12 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
1952
2224
|
try {
|
|
1953
2225
|
proc = await createExtensionProcessSandbox(this).exec(argv as unknown as SandboxCommand, launch);
|
|
1954
2226
|
} catch (err) {
|
|
2227
|
+
// A DO code-update reset (our own Worker deploy) BEFORE a runtime
|
|
2228
|
+
// replacement: the container is unchanged, so this is `control-reset`, not
|
|
2229
|
+
// the replaced word — never `swapIncarnation`, and the command's outcome
|
|
2230
|
+
// is unknown. Checked first so a DO reset over a live pi is never
|
|
2231
|
+
// read as a replaced container.
|
|
2232
|
+
if (isControlReset(err)) throw new ControlResetError("spawn", err);
|
|
1955
2233
|
if (!isRuntimeReplacement(err)) {
|
|
1956
2234
|
// The control port never answered the SDK's connect (its 30 s abort,
|
|
1957
2235
|
// raised inside the wake path): no process started and nothing about
|
|
@@ -1969,8 +2247,25 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
1969
2247
|
// takes the throw below. It exists so that if a future SDK vouches "never
|
|
1970
2248
|
// started" we retry then — and only then — without a change here.
|
|
1971
2249
|
if (!(err instanceof OperationInterruptedError && err.retryable === true)) {
|
|
1972
|
-
|
|
1973
|
-
|
|
2250
|
+
// Two consequences with different safety, split here. The memos
|
|
2251
|
+
// (`hydratedVerdictAt`, `gitSetupDone`, `stageDirsReady`, the deps
|
|
2252
|
+
// flags — the fact that was stale) clear on EVERY replacement the SDK
|
|
2253
|
+
// classified: cheap to re-derive, and the per-incarnation memo block's
|
|
2254
|
+
// invariant is that this one choke point clears them, whatever the
|
|
2255
|
+
// word will say. The incarnation — the lease fencing token, one
|
|
2256
|
+
// isolate paired with one container runtime — is minted only when the
|
|
2257
|
+
// replacement is KNOWN: `isRuntimeReplacement` is the union, and a
|
|
2258
|
+
// transport blip on one request that minted an id would make
|
|
2259
|
+
// `takeMutex` read the live holder's row as `holder-incarnation-gone`
|
|
2260
|
+
// and hand the mirror mutex to a concurrent `/attach` over a running
|
|
2261
|
+
// fetch. (A swap gated whole on `known` was tried and left the memos
|
|
2262
|
+
// stale on a real roll, where `known` is false while the wake rewrites
|
|
2263
|
+
// the base's state — resident-repos item 43.) Only the word is gated,
|
|
2264
|
+
// on the same `known`.
|
|
2265
|
+
this.clearIncarnationMemos();
|
|
2266
|
+
const known = await this.replacementKnown(err);
|
|
2267
|
+
if (known) this.swapIncarnation();
|
|
2268
|
+
throw new RuntimeReplacedError("spawn", err, known);
|
|
1974
2269
|
}
|
|
1975
2270
|
console.log(
|
|
1976
2271
|
`exec: runtime replaced before the process started (SDK says retryable) — retrying once: ${errMsg(err)}`,
|
|
@@ -1992,9 +2287,18 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
1992
2287
|
truncated: out.truncated,
|
|
1993
2288
|
};
|
|
1994
2289
|
} catch (err) {
|
|
2290
|
+
// A DO code-update reset during collect: the container and the process are
|
|
2291
|
+
// unchanged (only this DO's isolate reset), so the outcome is unknown but
|
|
2292
|
+
// never a replacement — `control-reset`, no `swapIncarnation`.
|
|
2293
|
+
// Checked before the replacement branch below.
|
|
2294
|
+
if (isControlReset(err)) throw new ControlResetError("collect", err);
|
|
1995
2295
|
if (isRuntimeReplacement(err)) {
|
|
1996
|
-
|
|
1997
|
-
|
|
2296
|
+
// The same split as the spawn site above: the memos unconditionally,
|
|
2297
|
+
// the incarnation only when the replacement is known, the word gated.
|
|
2298
|
+
this.clearIncarnationMemos();
|
|
2299
|
+
const known = await this.replacementKnown(err);
|
|
2300
|
+
if (known) this.swapIncarnation();
|
|
2301
|
+
throw new RuntimeReplacedError("collect", err, known);
|
|
1998
2302
|
}
|
|
1999
2303
|
if (err instanceof ProcessWaitTimeoutError) {
|
|
2000
2304
|
// The supervisor should have killed the process at `timeout`; 30 s
|
|
@@ -2574,7 +2878,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2574
2878
|
// asks the runtime whether the container the extract wrote to is still
|
|
2575
2879
|
// there (the incident that named this: a resident went `down` on exactly
|
|
2576
2880
|
// this, 10 ms before every exec answered "container is not running").
|
|
2577
|
-
|
|
2881
|
+
// A DO code-update reset under a live restore leaves nothing to land (the
|
|
2882
|
+
// restore's SDK call is gone with this isolate), so it shares the
|
|
2883
|
+
// replacement disposition — interrupted, retried onto the container that
|
|
2884
|
+
// comes back. This is the ONLY site that folds the DO reset in;
|
|
2885
|
+
// `isRuntimeReplacement` itself stays immune to it, so `/exec` never
|
|
2886
|
+
// answers the replaced word for a reset over a live container.
|
|
2887
|
+
const runtimeReplaced = isRuntimeReplacement(err) || isControlReset(err);
|
|
2578
2888
|
const runtimeActive = runtimeReplaced ? false : await this.isRuntimeActive().catch(() => null);
|
|
2579
2889
|
const disposition = restoreFailureDisposition(errMsg(err), {
|
|
2580
2890
|
runtimeReplaced,
|
|
@@ -3227,6 +3537,14 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3227
3537
|
await this.recoverFromDiskFull(failure.reason, selfInFlight);
|
|
3228
3538
|
return { ok: false, reason: failure.reason };
|
|
3229
3539
|
}
|
|
3540
|
+
// Item 67: a fetch that failed on the MIRROR (its remote gone, its
|
|
3541
|
+
// object store broken) is the resident's own step failing — `fetch-failed`,
|
|
3542
|
+
// counted by the ladder, never serviceable — not GitHub being unreachable.
|
|
3543
|
+
// The first live row found a broken mirror parked as `github-unreachable`.
|
|
3544
|
+
if (fetchFailureIsMirrors(message)) {
|
|
3545
|
+
await this.refreshFailed(failure, selfInFlight);
|
|
3546
|
+
return { ok: false, reason: failure.reason };
|
|
3547
|
+
}
|
|
3230
3548
|
const reason = `github-unreachable: ${message}`;
|
|
3231
3549
|
await this.setResidentState("degraded", reason);
|
|
3232
3550
|
return { ok: false, reason };
|
|
@@ -4353,7 +4671,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4353
4671
|
const reason = diskPressureReason({ verdict: final, evicted, kept, spares });
|
|
4354
4672
|
console.log(`attach ${input.threadKey}: ${reason}`);
|
|
4355
4673
|
const s = await this.getStatus();
|
|
4356
|
-
return { error: reason, status: 503, state: s.state, reason: DISK_PRESSURE_REASON };
|
|
4674
|
+
return { error: reason, status: 503, state: s.state, stateReason: s.reason, reason: DISK_PRESSURE_REASON };
|
|
4357
4675
|
}
|
|
4358
4676
|
const m = final.math;
|
|
4359
4677
|
console.log(
|
|
@@ -4753,6 +5071,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4753
5071
|
error: "image-stale: the container predates the current pool and is restarting; retry shortly",
|
|
4754
5072
|
status: 503,
|
|
4755
5073
|
state: "restoring",
|
|
5074
|
+
stateReason: "",
|
|
4756
5075
|
reason: "image-stale",
|
|
4757
5076
|
};
|
|
4758
5077
|
}
|
|
@@ -4780,7 +5099,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4780
5099
|
this.attachesInFlight--;
|
|
4781
5100
|
}
|
|
4782
5101
|
} catch (err) {
|
|
4783
|
-
return
|
|
5102
|
+
return catchAllErr(err, "attach-failed");
|
|
4784
5103
|
}
|
|
4785
5104
|
}
|
|
4786
5105
|
|
|
@@ -4792,7 +5111,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4792
5111
|
* instance's entry gate (the cron's, within one bucket) — an attach never
|
|
4793
5112
|
* stops the container itself: it is in flight. */
|
|
4794
5113
|
private async attachFailed(err: unknown): Promise<ThreadErr> {
|
|
4795
|
-
if (!(err instanceof StepError)) return
|
|
5114
|
+
if (!(err instanceof StepError)) return catchAllErr(err, "attach-failed");
|
|
4796
5115
|
const failure = await this.classifyFailure(err.step, err.message);
|
|
4797
5116
|
if (!failure.diskFull) return { error: `attach-failed at ${err.step}: ${err.message}`, status: 500 };
|
|
4798
5117
|
console.log(`attach: ${failure.reason}`);
|
|
@@ -4816,7 +5135,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4816
5135
|
await this.refreshIfStale(resourceId);
|
|
4817
5136
|
} catch (err) {
|
|
4818
5137
|
const s = await this.getStatus();
|
|
4819
|
-
return {
|
|
5138
|
+
return {
|
|
5139
|
+
error: `not-serviceable: ${errMsg(err)}`,
|
|
5140
|
+
status: 503,
|
|
5141
|
+
state: s.state,
|
|
5142
|
+
stateReason: s.reason,
|
|
5143
|
+
reason: s.reason,
|
|
5144
|
+
};
|
|
4820
5145
|
}
|
|
4821
5146
|
// `resourceId` was read by the caller a moment ago (item 15 of the audit:
|
|
4822
5147
|
// this used to re-read the same key), the registry record rides in from
|
|
@@ -4828,7 +5153,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4828
5153
|
const stored = await this.ctx.storage.get<RepoFacts | ThreadBinding>([FACTS_KEY, threadBindingKey(threadKey)]);
|
|
4829
5154
|
const facts = stored.get(FACTS_KEY) as RepoFacts | undefined;
|
|
4830
5155
|
const record = recordFromRoute ?? (await this.registry().getRecord(resource));
|
|
4831
|
-
|
|
5156
|
+
// Typed `reason` beside the words: the client reads the field — a refusal no
|
|
5157
|
+
// wait clears, unlike the restore window's 503s — never the sentence.
|
|
5158
|
+
if (!record || !facts)
|
|
5159
|
+
return { error: "not-serviceable: registry record or repo facts missing", status: 503, reason: "unregistered" };
|
|
4832
5160
|
|
|
4833
5161
|
// The binding's ref wins for the thread's whole life, with one exception
|
|
4834
5162
|
// (item 16): a thread bound to the repo default for want of a named branch
|
|
@@ -4978,9 +5306,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4978
5306
|
} catch (err) {
|
|
4979
5307
|
if (err instanceof MirrorBusyError) {
|
|
4980
5308
|
const s = await this.getStatus();
|
|
4981
|
-
return { error: errMsg(err), status: 503, state: s.state, reason: "mirror-busy" };
|
|
5309
|
+
return { error: errMsg(err), status: 503, state: s.state, stateReason: s.reason, reason: "mirror-busy" };
|
|
4982
5310
|
}
|
|
4983
|
-
return
|
|
5311
|
+
return catchAllErr(err, "attach-failed");
|
|
4984
5312
|
}
|
|
4985
5313
|
if (outcome.kind === "none") return { binding: outcome.binding };
|
|
4986
5314
|
if (outcome.kind === "refuse") {
|
|
@@ -5280,7 +5608,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5280
5608
|
return { error: `reuse-refused: ${err.why}`, status: 409, needs: "recreate" };
|
|
5281
5609
|
if (err instanceof MirrorBusyError) {
|
|
5282
5610
|
const s = await this.getStatus();
|
|
5283
|
-
return { error: errMsg(err), status: 503, state: s.state, reason: "mirror-busy" };
|
|
5611
|
+
return { error: errMsg(err), status: 503, state: s.state, stateReason: s.reason, reason: "mirror-busy" };
|
|
5284
5612
|
}
|
|
5285
5613
|
if (err instanceof StepError && err.step === "unknown-ref") {
|
|
5286
5614
|
return { error: `unknown-ref: ${err.message}`, status: 400 };
|
|
@@ -5325,7 +5653,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5325
5653
|
// worktree lock above, so the bot-side fallback can retry.
|
|
5326
5654
|
if (err instanceof MirrorBusyError) {
|
|
5327
5655
|
const s = await this.getStatus();
|
|
5328
|
-
return { error: errMsg(err), status: 503, state: s.state, reason: "mirror-busy" };
|
|
5656
|
+
return { error: errMsg(err), status: 503, state: s.state, stateReason: s.reason, reason: "mirror-busy" };
|
|
5329
5657
|
}
|
|
5330
5658
|
return this.attachFailed(err);
|
|
5331
5659
|
}
|
|
@@ -6163,7 +6491,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6163
6491
|
await this.ensureHydrated();
|
|
6164
6492
|
} catch (err) {
|
|
6165
6493
|
const s = await this.getStatus();
|
|
6166
|
-
return {
|
|
6494
|
+
return {
|
|
6495
|
+
error: `not-serviceable: ${errMsg(err)}`,
|
|
6496
|
+
status: 503,
|
|
6497
|
+
state: s.state,
|
|
6498
|
+
stateReason: s.reason,
|
|
6499
|
+
reason: s.reason,
|
|
6500
|
+
};
|
|
6167
6501
|
}
|
|
6168
6502
|
const binding = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(threadKey));
|
|
6169
6503
|
if (!binding) {
|
|
@@ -6233,11 +6567,33 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6233
6567
|
return res;
|
|
6234
6568
|
}
|
|
6235
6569
|
|
|
6570
|
+
/** The exec route's one gate for a runtime replacement (item 43): every
|
|
6571
|
+
* `RuntimeReplacedError` the route meets — the preflight's own probe of the
|
|
6572
|
+
* worktree, the command itself — is judged by `replacedExecAnswer`, so the
|
|
6573
|
+
* word reaches the harness only when the SDK vouched the runtime moved or
|
|
6574
|
+
* the resident knows the container it held is gone. */
|
|
6236
6575
|
private async execThreadImpl(
|
|
6237
6576
|
threadKey: string,
|
|
6238
6577
|
command: string,
|
|
6239
6578
|
timeoutMs: number,
|
|
6240
6579
|
env?: Record<string, string>,
|
|
6580
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number; truncated: boolean } | ThreadErr> {
|
|
6581
|
+
try {
|
|
6582
|
+
return await this.execThreadBody(threadKey, command, timeoutMs, env);
|
|
6583
|
+
} catch (err) {
|
|
6584
|
+
// A DO reset over a live container answers its own word at once — the
|
|
6585
|
+
// container is unchanged, so no `replacedExecAnswer` gate applies.
|
|
6586
|
+
if (err instanceof ControlResetError) return controlResetErr(err);
|
|
6587
|
+
if (err instanceof RuntimeReplacedError) return this.replacedExecAnswer(err);
|
|
6588
|
+
throw err;
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
|
|
6592
|
+
private async execThreadBody(
|
|
6593
|
+
threadKey: string,
|
|
6594
|
+
command: string,
|
|
6595
|
+
timeoutMs: number,
|
|
6596
|
+
env?: Record<string, string>,
|
|
6241
6597
|
): Promise<{ stdout: string; stderr: string; exitCode: number; truncated: boolean } | ThreadErr> {
|
|
6242
6598
|
const pre = await this.threadPreflight(threadKey);
|
|
6243
6599
|
if ("error" in pre) return pre;
|
|
@@ -6253,13 +6609,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6253
6609
|
: {}),
|
|
6254
6610
|
} satisfies ThreadBinding);
|
|
6255
6611
|
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
} catch (err) {
|
|
6260
|
-
if (err instanceof RuntimeReplacedError) return runtimeReplacedErr(err);
|
|
6261
|
-
throw err;
|
|
6262
|
-
}
|
|
6612
|
+
// A runtime replacement or a control reset under the command propagates to
|
|
6613
|
+
// `execThread`'s gate (`replacedExecAnswer` / `controlResetErr`).
|
|
6614
|
+
const r = await this.threadRunCapped(binding.user, binding.worktreePath, command, timeoutMs, EXEC_OUTPUT_CAP, env);
|
|
6263
6615
|
const truncated = r.stdout.length > EXEC_OUTPUT_CAP || r.stderr.length > EXEC_OUTPUT_CAP || r.truncated === true;
|
|
6264
6616
|
const notes: string[] = [];
|
|
6265
6617
|
if (r.timedOut)
|
|
@@ -6311,6 +6663,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6311
6663
|
capBytesFor(READ_CONTENT_CAP),
|
|
6312
6664
|
);
|
|
6313
6665
|
} catch (err) {
|
|
6666
|
+
if (err instanceof ControlResetError) return controlResetErr(err);
|
|
6314
6667
|
if (err instanceof RuntimeReplacedError) return runtimeReplacedErr(err);
|
|
6315
6668
|
throw err;
|
|
6316
6669
|
}
|
|
@@ -6359,6 +6712,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6359
6712
|
}
|
|
6360
6713
|
return { encoding: "base64", content: parts.join(""), size };
|
|
6361
6714
|
} catch (err) {
|
|
6715
|
+
if (err instanceof ControlResetError) return controlResetErr(err);
|
|
6362
6716
|
if (err instanceof RuntimeReplacedError) return runtimeReplacedErr(err);
|
|
6363
6717
|
throw err;
|
|
6364
6718
|
}
|
|
@@ -6405,6 +6759,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6405
6759
|
DEFAULT_EXEC_TIMEOUT_MS,
|
|
6406
6760
|
);
|
|
6407
6761
|
} catch (err) {
|
|
6762
|
+
if (err instanceof ControlResetError) return controlResetErr(err);
|
|
6408
6763
|
if (err instanceof RuntimeReplacedError) return runtimeReplacedErr(err);
|
|
6409
6764
|
const step = err instanceof StepError ? ` at ${err.step}` : "";
|
|
6410
6765
|
return { error: `write-failed${step}: ${errMsg(err)}`, status: 400 };
|
|
@@ -6771,7 +7126,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6771
7126
|
await this.ensureHydrated();
|
|
6772
7127
|
} catch (err) {
|
|
6773
7128
|
const s = await this.getStatus();
|
|
6774
|
-
return {
|
|
7129
|
+
return {
|
|
7130
|
+
error: `not-serviceable: ${errMsg(err)}`,
|
|
7131
|
+
status: 503,
|
|
7132
|
+
state: s.state,
|
|
7133
|
+
stateReason: s.reason,
|
|
7134
|
+
reason: s.reason,
|
|
7135
|
+
};
|
|
6775
7136
|
}
|
|
6776
7137
|
// One storage round trip for the two facts; the registry lookup stays (an
|
|
6777
7138
|
// op resolves ONLY through the onboard-time command table).
|
|
@@ -6779,7 +7140,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6779
7140
|
const resource = (stored.get(RESOURCE_KEY) as string | undefined) ?? "";
|
|
6780
7141
|
const facts = stored.get(FACTS_KEY) as RepoFacts | undefined;
|
|
6781
7142
|
const record = await this.registry().getRecord(resource);
|
|
6782
|
-
|
|
7143
|
+
// Typed `reason` beside the words: the client reads the field — a refusal no
|
|
7144
|
+
// wait clears, unlike the restore window's 503s — never the sentence.
|
|
7145
|
+
if (!record || !facts)
|
|
7146
|
+
return { error: "not-serviceable: registry record or repo facts missing", status: 503, reason: "unregistered" };
|
|
6783
7147
|
const command = record.commands[op];
|
|
6784
7148
|
if (!command) return { error: `op-unavailable: the command table has no "${op}" entry`, status: 400 };
|
|
6785
7149
|
|
|
@@ -6875,13 +7239,15 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6875
7239
|
} catch (err) {
|
|
6876
7240
|
if (err instanceof MirrorBusyError) {
|
|
6877
7241
|
const s = await this.getStatus();
|
|
6878
|
-
return { error: errMsg(err), status: 503, state: s.state, reason: "mirror-busy" };
|
|
7242
|
+
return { error: errMsg(err), status: 503, state: s.state, stateReason: s.reason, reason: "mirror-busy" };
|
|
6879
7243
|
}
|
|
6880
7244
|
if (err instanceof StepError && err.step === "unknown-ref") {
|
|
6881
7245
|
return { error: `unknown-ref: ${err.message}`, status: 400 };
|
|
6882
7246
|
}
|
|
6883
|
-
|
|
6884
|
-
|
|
7247
|
+
// A step that failed is named and deterministic; a throw no step named is
|
|
7248
|
+
// typed by the one builder every such 500 goes through.
|
|
7249
|
+
if (err instanceof StepError) return { error: `op-failed at ${err.step}: ${errMsg(err)}`, status: 500 };
|
|
7250
|
+
return catchAllErr(err, "op-failed");
|
|
6885
7251
|
} finally {
|
|
6886
7252
|
// Disposable means disposable: the checkout dies with the op, pass or
|
|
6887
7253
|
// fail (best effort — a slept container already destroyed it anyway).
|
|
@@ -7035,13 +7401,22 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7035
7401
|
fates.set(ref, { fate: looked.fate, detail });
|
|
7036
7402
|
}),
|
|
7037
7403
|
);
|
|
7404
|
+
// The runs registered on this resident (item 44): a run holds its tree
|
|
7405
|
+
// from its attach to its release, and its process lives in the container
|
|
7406
|
+
// between the bot's calls — the op counters read 0 while its model thinks.
|
|
7407
|
+
const registered = new Set(
|
|
7408
|
+
[...(await this.ctx.storage.list<RunRegistration>({ prefix: RUN_REG_KEY_PREFIX })).values()].map(
|
|
7409
|
+
(r) => r.threadKey,
|
|
7410
|
+
),
|
|
7411
|
+
);
|
|
7038
7412
|
for (const binding of live) {
|
|
7039
7413
|
const isDefaultRef = binding.ref === defaultRef;
|
|
7040
7414
|
// The default branch is never a finished ref (reclaimDecision keeps it
|
|
7041
7415
|
// by name), so its fate is never looked up.
|
|
7042
7416
|
const { fate, detail } = (!isDefaultRef && fates.get(binding.ref)) || { fate: "unknown" as RefFate, detail: "" };
|
|
7043
7417
|
const busy = this.threadOpsInFlight.get(binding.threadKey) ?? 0;
|
|
7044
|
-
const
|
|
7418
|
+
const held = registered.has(binding.threadKey);
|
|
7419
|
+
const decision = reclaimDecision({ fate, isDefaultRef, busy, held });
|
|
7045
7420
|
if (!decision.reclaim) {
|
|
7046
7421
|
kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: decision.why });
|
|
7047
7422
|
continue;
|
|
@@ -7058,6 +7433,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7058
7433
|
kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: "busy" });
|
|
7059
7434
|
continue;
|
|
7060
7435
|
}
|
|
7436
|
+
// A run attached while the tree was measured holds it now (item 44).
|
|
7437
|
+
if ((await this.ctx.storage.get<RunRegistration>(runRegKey(binding.threadKey))) !== undefined) {
|
|
7438
|
+
kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: "run-held" });
|
|
7439
|
+
continue;
|
|
7440
|
+
}
|
|
7061
7441
|
if (!current || current.evicted || current.lastAttachAt !== binding.lastAttachAt) {
|
|
7062
7442
|
kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: "re-attached" });
|
|
7063
7443
|
continue;
|
|
@@ -7411,13 +7791,17 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7411
7791
|
return { infraStreak: { ...row, rung: infraStreakRung(count) } };
|
|
7412
7792
|
}
|
|
7413
7793
|
|
|
7414
|
-
/** Fault injection for item 67: corrupt the mirror on disk
|
|
7415
|
-
*
|
|
7416
|
-
*
|
|
7794
|
+
/** Fault injection for item 67: corrupt the mirror on disk so the next
|
|
7795
|
+
* cycle's `fetch` fails in a resident step. The mirror's `config` goes (its
|
|
7796
|
+
* `origin` remote with it: `'origin' does not appear to be a git
|
|
7797
|
+
* repository`, exit 128) — NOT its objects: the wake check (`readyStamp`)
|
|
7798
|
+
* tests `objects/`, and a missing directory is a disk to restore, which the
|
|
7799
|
+
* wake did before any step could fail the first time this was tried live.
|
|
7800
|
+
* The snapshots in R2 are untouched — the ladder's recreate restores them.
|
|
7417
7801
|
* Test-only semantics; admin scope. */
|
|
7418
7802
|
async debugBreakMirror(): Promise<{ broken: boolean; error?: string }> {
|
|
7419
7803
|
try {
|
|
7420
|
-
await this.runOk(["rm", "-
|
|
7804
|
+
await this.runOk(["rm", "-f", `${MIRROR_DIR}/config`], "break-mirror");
|
|
7421
7805
|
return { broken: true };
|
|
7422
7806
|
} catch (err) {
|
|
7423
7807
|
return { broken: false, error: errMsg(err) };
|
|
@@ -7960,7 +8344,7 @@ export default {
|
|
|
7960
8344
|
return json({ error: "unknown route" }, 404);
|
|
7961
8345
|
}
|
|
7962
8346
|
} catch (err) {
|
|
7963
|
-
return json(
|
|
8347
|
+
return json(catchAllErr(err), 500);
|
|
7964
8348
|
}
|
|
7965
8349
|
})();
|
|
7966
8350
|
root?.end(res.status >= 500 ? "error" : "ok", { httpStatus: res.status });
|
|
@@ -8510,7 +8894,7 @@ async function handleAttach(env: Env, body: Record<string, unknown>, traceparent
|
|
|
8510
8894
|
reason,
|
|
8511
8895
|
),
|
|
8512
8896
|
(result) => result,
|
|
8513
|
-
(err) => (
|
|
8897
|
+
(err) => catchAllErr(err),
|
|
8514
8898
|
);
|
|
8515
8899
|
}
|
|
8516
8900
|
|
|
@@ -8528,7 +8912,7 @@ async function handleAwaitRestore(env: Env, body: Record<string, unknown>): Prom
|
|
|
8528
8912
|
return streamHeartbeatJson(
|
|
8529
8913
|
residentStub(env, resource.resource).awaitRestore(),
|
|
8530
8914
|
(result) => result,
|
|
8531
|
-
(err) => (
|
|
8915
|
+
(err) => catchAllErr(err),
|
|
8532
8916
|
);
|
|
8533
8917
|
}
|
|
8534
8918
|
|
|
@@ -8606,33 +8990,50 @@ function streamHeartbeatJson<T>(
|
|
|
8606
8990
|
return new Response(stream, { headers: { "content-type": "application/json" } });
|
|
8607
8991
|
}
|
|
8608
8992
|
|
|
8609
|
-
/** /exec's
|
|
8610
|
-
*
|
|
8611
|
-
*
|
|
8612
|
-
*
|
|
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
|
+
/** /exec's payload mapping: a result as {stdout, stderr, exitCode, truncated};
|
|
9021
|
+
* a failure as `execFailureDocument` — the one the Durable Object named, or
|
|
9022
|
+
* the pending result's rejection answered as the DO would have
|
|
9023
|
+
* (`threadRejectionErr`: a code-update reset the DO's own `control-reset`
|
|
9024
|
+
* word, a replacement the SDK's words on a bare 409 — the word withheld on
|
|
9025
|
+
* /exec alone —, anything else the typed 500), so a rejection streams its
|
|
9026
|
+
* `status`, its word and its `transient`,
|
|
9027
|
+
* and the client reads it like the JSON routes' answer, never as a
|
|
9028
|
+
* deterministic answer over HTTP 200. */
|
|
8613
9029
|
function streamThreadExec(pending: Promise<Awaited<ReturnType<ResidentDO["execThread"]>>>): Response {
|
|
8614
9030
|
return streamHeartbeatJson(
|
|
8615
9031
|
pending,
|
|
8616
9032
|
(result) =>
|
|
8617
9033
|
"error" in result
|
|
8618
|
-
?
|
|
8619
|
-
error: result.error,
|
|
8620
|
-
...(result.needs ? { needs: result.needs } : {}),
|
|
8621
|
-
// `reason` must stay independent of `state`: runtimeReplacedErr()
|
|
8622
|
-
// sets reason:"runtime-replaced" with NO state, and the client's
|
|
8623
|
-
// deploy-vs-dead-transport check reads it. Folding these two spreads
|
|
8624
|
-
// back into one silently drops it (no test covers this Worker).
|
|
8625
|
-
...(result.state ? { state: result.state } : {}),
|
|
8626
|
-
...(result.reason ? { reason: result.reason } : {}),
|
|
8627
|
-
stdout: "",
|
|
8628
|
-
stderr: result.error,
|
|
8629
|
-
exitCode: 127,
|
|
8630
|
-
}
|
|
9034
|
+
? execFailureDocument(result)
|
|
8631
9035
|
: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, truncated: result.truncated },
|
|
8632
|
-
(err) =>
|
|
8633
|
-
const msg = errMsg(err);
|
|
8634
|
-
return { error: msg, stdout: "", stderr: msg, exitCode: 127 };
|
|
8635
|
-
},
|
|
9036
|
+
(err) => execFailureDocument(threadRejectionErr(err, "/exec")),
|
|
8636
9037
|
);
|
|
8637
9038
|
}
|
|
8638
9039
|
|
|
@@ -8643,7 +9044,12 @@ async function handleRead(env: Env, body: Record<string, unknown>): Promise<Resp
|
|
|
8643
9044
|
return json({ error: "path must be a string relative to the thread worktree" }, 400);
|
|
8644
9045
|
const encoding = readEncodingOf(body);
|
|
8645
9046
|
if (typeof encoding !== "string") return json({ error: encoding.error }, 400);
|
|
8646
|
-
|
|
9047
|
+
// A stub that rejects (the DO reset under the call, a storage operation that
|
|
9048
|
+
// did not complete) is answered as the DO answers the same fact inside, never
|
|
9049
|
+
// left to the fetch handler's catch-all.
|
|
9050
|
+
const result = await ctx.stub
|
|
9051
|
+
.readThreadFile(ctx.threadKey, body.path, encoding)
|
|
9052
|
+
.catch((err: unknown) => threadRejectionErr(err, "/read"));
|
|
8647
9053
|
if ("error" in result) return threadErrResponse(result);
|
|
8648
9054
|
return json(result);
|
|
8649
9055
|
}
|
|
@@ -8656,7 +9062,9 @@ async function handleWrite(env: Env, body: Record<string, unknown>): Promise<Res
|
|
|
8656
9062
|
if (typeof body.content !== "string" || body.content.length > MAX_WRITE_CONTENT) {
|
|
8657
9063
|
return json({ error: `content must be a string of at most ${MAX_WRITE_CONTENT} chars` }, 400);
|
|
8658
9064
|
}
|
|
8659
|
-
const result = await ctx.stub
|
|
9065
|
+
const result = await ctx.stub
|
|
9066
|
+
.writeThreadFile(ctx.threadKey, body.path, body.content)
|
|
9067
|
+
.catch((err: unknown) => threadRejectionErr(err, "/write"));
|
|
8660
9068
|
if ("error" in result) return threadErrResponse(result);
|
|
8661
9069
|
return json(result);
|
|
8662
9070
|
}
|
|
@@ -8730,18 +9138,18 @@ async function handleOp(env: Env, body: Record<string, unknown>, traceparent?: s
|
|
|
8730
9138
|
}
|
|
8731
9139
|
|
|
8732
9140
|
/** /op's payload mapping: results pass through; a named error sheds its
|
|
8733
|
-
* transport-only `status` field (the body is the contract, never the code)
|
|
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. */
|
|
8734
9144
|
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
|
+
};
|
|
8735
9149
|
return streamHeartbeatJson(
|
|
8736
9150
|
pending,
|
|
8737
|
-
(result) =>
|
|
8738
|
-
|
|
8739
|
-
const { status: _status, ...rest } = result;
|
|
8740
|
-
return rest;
|
|
8741
|
-
}
|
|
8742
|
-
return result;
|
|
8743
|
-
},
|
|
8744
|
-
(err) => ({ error: errMsg(err) }),
|
|
9151
|
+
(result) => ("error" in result ? shedStatus(result) : result),
|
|
9152
|
+
(err) => shedStatus(catchAllErr(err, "op-failed")),
|
|
8745
9153
|
);
|
|
8746
9154
|
}
|
|
8747
9155
|
|