@coreplane/switchboard 1.243.0 → 1.245.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 +6 -3
- package/dist/assets/deploy/cloudflare/worker.ts +2 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +148 -0
- package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
- package/dist/assets/deploy/cloudflare-resident/threadErr.ts +337 -0
- package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +258 -173
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +18 -12
- package/dist/assets/deploy/secrets.manifest.json +12 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +5 -5
- package/dist/assets/src/config/profile.ts +17 -1
- package/dist/assets/src/core/authz/types.ts +18 -0
- package/dist/assets/src/core/budgets.ts +61 -0
- package/dist/assets/src/core/coordinator/contract.ts +32 -0
- package/dist/assets/src/core/coordinator/driver.ts +75 -12
- package/dist/assets/src/core/delivery.ts +1 -1
- package/dist/assets/src/core/reviewVerdict.ts +101 -24
- package/dist/assets/src/core/runEvents.ts +76 -8
- package/dist/assets/src/core/runFriction.ts +2 -1
- package/dist/assets/src/core/runRecord.ts +65 -0
- package/dist/assets/src/core/ship/contract.ts +2 -1
- package/dist/assets/src/core/ship/coordinator.ts +235 -33
- 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/residentRefresh.ts +46 -7
- 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 +491 -472
- package/dist/assets/web/dist/assets/AppShell-qqs_p96-.js +1 -0
- package/dist/assets/web/dist/assets/CostsPage-Dd9aXxxI.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-VLTFopnl.js +2 -0
- package/dist/assets/web/dist/assets/InputMenu-lQP3ia9D.js +1 -0
- package/dist/assets/web/dist/assets/NotFoundPage-8ViskpN3.js +1 -0
- package/dist/assets/web/dist/assets/PendingTurnRow-D2gHef5v.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-nWLN8BOd.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-D3Q48CJW.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-CHy_IfZG.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-C6kX5om4.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-WyeG5tKb.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-B8D5AU4d.js +1 -0
- package/dist/assets/web/dist/assets/SettingSelect-UTHsqUUS.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-BoW7Zjzt.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-BGHureCJ.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-CiP8qPd7.js +1 -0
- package/dist/assets/web/dist/assets/UnitRoutePage-2zEeRBP1.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-qXhb9ti8.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-DzvdyoUZ.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-Ck7Ru2Sm.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-CwehvReb.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-WSyVbSFK.js} +1 -1
- package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DMHI1wDz.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-BF8AYK8_.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-UAfImuWu.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-D54hi96d.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-BRWfLgBc.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-CSqC5deT.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-D4ZuRFGk.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-twkFmSUY.js → dist-73ZMn_Mg.js} +2 -2
- package/dist/assets/web/dist/assets/{durationTone-BobbycC-.js → durationTone-D51enaGR.js} +1 -1
- package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-DDPtNbZc.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-BTQkQfsJ.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-B_PkLzGZ.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-DfjIMVM_.js} +1 -1
- package/dist/assets/web/dist/assets/{eventSource-tRprhqm7.js → eventSource-By0B_w4S.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DP8wmn7K.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-G12VK2gK.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-D3XiSI8E.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DOKAhMj-.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CJB_fmON.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BHqtRWap.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-DwotyarS.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-B6fDaA3U.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-C5bpQmmI.js} +1 -1
- package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-s52taj36.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-99zC935r.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-Bgk-4-0x.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-B_s5tKyq.js → indexRow-BmK74Vp1.js} +1 -1
- package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-CuV_c5I0.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-CZlOvSMQ.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-BuXeZ4Tu.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-DIuP0hkG.js} +1 -1
- package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-CZ74vfoD.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-BNjRs8Vj.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-OCE-fFOg.js} +1 -1
- package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-CtvD87WF.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-C3kf86OR.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-B1WI--Kp.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-DtoT0gKg.js} +1 -1
- package/dist/assets/web/dist/assets/main-ClluP-Qf.js +28 -0
- package/dist/assets/web/dist/assets/main-CpkwzHX6.css +1 -0
- package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-BE2MC6Yt.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-BwO4weXJ.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-BTuOgGVC.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-CiIUM-oV.js} +1 -1
- package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-L78DEWB-.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-KqHOm4Ft.js} +1 -1
- package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-C41XbEkn.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-B12-4i-k.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml--fw2Up5c.js} +1 -1
- package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-Db1JBXrC.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-BYpVBgbu.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-BMqGLSPc.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-B63n7-Sa.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-D3VI8UYv.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DxRf8ZAR.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DRRA6d6A.js} +1 -1
- package/dist/assets/web/dist/assets/seed-DFfT5S_N.js +6 -0
- package/dist/assets/web/dist/assets/{settingsApi-priOPiwu.js → settingsApi-Dm6eaQcu.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CoOBe3_y.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DOV1HYxf.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-Acpmpp1W.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-DeT9N-fA.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-C9m_EB8J.js → sseReplay-DPXVxhL5.js} +5 -5
- package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CbxXAMO4.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-BdKbi4hc.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-tAcjDM7R.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CObINNJj.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-BYwpZT1J.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags-BciREL5R.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-CRiv3Pr3.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-DuZZ1Ej0.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-jkHz_5HZ.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-DOac4Gal.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-CgOX5wXA.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-Duu99VNw.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-Bmj_wvRJ.js} +1 -1
- package/dist/assets/web/dist/assets/wallClock-DD_fPuSY.js +1 -0
- package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-BtUIVeWH.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-wX71n1E5.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-CdLdXvmz.js} +1 -1
- package/dist/cli.js +4330 -1256
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-IDMGG6yi.js +0 -1
- package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +0 -2
- package/dist/assets/web/dist/assets/NotFoundPage-C97EhJcQ.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DACalNVF.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +0 -1
- package/dist/assets/web/dist/assets/RunsTabs-BUfdk0lH.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +0 -1
- package/dist/assets/web/dist/assets/StatusDot-Dw0T1M-P.js +0 -1
- package/dist/assets/web/dist/assets/Tooltip-BbLuIAiS.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +0 -1
- package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +0 -1
- package/dist/assets/web/dist/assets/main-d-w-tIKt.js +0 -28
- package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +0 -6
- package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +0 -1
|
@@ -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,
|
|
@@ -80,7 +81,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
80
81
|
import type { DirectoryBackup, SandboxCommand } from "@cloudflare/sandbox";
|
|
81
82
|
import { createExtensionProcessSandbox } from "@cloudflare/sandbox/extensions";
|
|
82
83
|
import { DurableObject } from "cloudflare:workers";
|
|
83
|
-
import { BASH_TIMEOUT_MAX_MS,
|
|
84
|
+
import { BASH_TIMEOUT_MAX_MS, clampBashTimeout } from "../../src/execution/bashTimeout.js";
|
|
84
85
|
import { selectBindingsToPurge } from "../../src/execution/bindingPurge.js";
|
|
85
86
|
import { busyAfterKillReason, planForceDetach } from "../../src/execution/residentDetach.js";
|
|
86
87
|
import { parseReadonly, planReadonlyAttach } from "../../src/execution/residentReadonly.js";
|
|
@@ -196,6 +197,7 @@ import {
|
|
|
196
197
|
type RefreshPlan,
|
|
197
198
|
type RestoreSample,
|
|
198
199
|
type RefreshFailure,
|
|
200
|
+
fetchFailureIsMirrors,
|
|
199
201
|
} from "../../src/execution/residentRefresh.js";
|
|
200
202
|
import { autoRebuildDecision, isAutoRebuildEligible } from "../../src/execution/residentAutoRebuild.js";
|
|
201
203
|
import {
|
|
@@ -321,6 +323,16 @@ import {
|
|
|
321
323
|
} from "../../src/execution/residentDepsStore.js";
|
|
322
324
|
import { buildId, injectedBuildStamp } from "../../src/deploy/buildStamp.js";
|
|
323
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";
|
|
324
336
|
import {
|
|
325
337
|
DEFAULT_EXEC_TIMEOUT_MS,
|
|
326
338
|
DEPS_STEP_OVERHEAD_MS,
|
|
@@ -619,9 +631,11 @@ const ATTACH_MUTEX_WAIT_MS = 60_000;
|
|
|
619
631
|
* stays far inside the bot's 45-min run budget. Work beyond 20 minutes
|
|
620
632
|
* belongs in background jobs.
|
|
621
633
|
*
|
|
622
|
-
* /op runs (test/build)
|
|
623
|
-
* path has no caller-supplied knob,
|
|
624
|
-
|
|
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;
|
|
625
639
|
/** Sanity bound on /exec's command body — a guard against a runaway caller,
|
|
626
640
|
* not a working limit: legitimate agent one-liners (heredocs writing test
|
|
627
641
|
* files, `node -e` scripts, long pipelines) run well past the old 8 000 and
|
|
@@ -672,28 +686,10 @@ export function validateEnvNames(vars: Record<string, string>): void {
|
|
|
672
686
|
}
|
|
673
687
|
}
|
|
674
688
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
* (the start RPC itself; the SDK never proves the process did NOT start) or
|
|
680
|
-
* `"collect"` (a `StaleProcessHandleError` on an already-running process). In
|
|
681
|
-
* both cases the command may have run, so the resident never re-issues it;
|
|
682
|
-
* the thread routes answer the NAMED `runtime-replaced` error and the client
|
|
683
|
-
* decides (idempotent read/write retry; exec is handed to the model). */
|
|
684
|
-
class RuntimeReplacedError extends Error {
|
|
685
|
-
constructor(
|
|
686
|
-
readonly phase: "spawn" | "collect",
|
|
687
|
-
readonly cause: unknown,
|
|
688
|
-
) {
|
|
689
|
-
super(
|
|
690
|
-
`runtime-replaced: the resident runtime was replaced (a deploy) while this command was ${
|
|
691
|
-
phase === "spawn" ? "starting" : "running"
|
|
692
|
-
}; its output is lost (${errMsg(cause)})`,
|
|
693
|
-
);
|
|
694
|
-
this.name = "RuntimeReplacedError";
|
|
695
|
-
}
|
|
696
|
-
}
|
|
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).
|
|
697
693
|
|
|
698
694
|
/** Every wording the pinned SDK (@cloudflare/sandbox@0.13.0-next.751.1) uses
|
|
699
695
|
* when the runtime incarnation changed under a call, for the message-based
|
|
@@ -711,17 +707,6 @@ class RuntimeReplacedError extends Error {
|
|
|
711
707
|
// port-ready bound. Both take the message fallback below; neither has a typed
|
|
712
708
|
// class to match.
|
|
713
709
|
|
|
714
|
-
/** `err` and its `cause` chain, bounded like the SDK's own `selfAndCauses`
|
|
715
|
-
* walker: the SDK wraps platform errors, so the telling message can sit one or
|
|
716
|
-
* two links down. */
|
|
717
|
-
function* selfAndCauses(err: unknown): Generator<unknown> {
|
|
718
|
-
let current = err;
|
|
719
|
-
for (let depth = 0; depth < 8 && current != null; depth++) {
|
|
720
|
-
yield current;
|
|
721
|
-
current = typeof current === "object" ? (current as { cause?: unknown }).cause : undefined;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
710
|
/** `OperationInterruptedError` reasons that mean the runtime under the call is
|
|
726
711
|
* gone and a new one serves the thread: a deploy swapped the isolate
|
|
727
712
|
* (`runtime_replaced`), the sandbox's lifetime epoch moved
|
|
@@ -753,16 +738,25 @@ const RPC_TRANSPORT_LOSS_KINDS = new Set(["peer_closed", "connection_failed", "u
|
|
|
753
738
|
/** Does this SDK error mean the runtime incarnation changed under us? Typed
|
|
754
739
|
* checks first (`StaleProcessHandleError`, `RuntimeIdentityInactiveError`, an
|
|
755
740
|
* `OperationInterruptedError` with one of `RUNTIME_REPLACED_REASONS`, an
|
|
756
|
-
* `RPCTransportError` with one of `RPC_TRANSPORT_LOSS_KINDS
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
741
|
+
* `RPCTransportError` with one of `RPC_TRANSPORT_LOSS_KINDS`), then the SDK's
|
|
742
|
+
* message wording — on the error AND its cause chain — as a belt-and-braces
|
|
743
|
+
* fallback. Anything else (a real spawn failure, a timeout, a wire-format
|
|
744
|
+
* error) is NOT a runtime replacement.
|
|
745
|
+
*
|
|
746
|
+
* Deliberately IMMUNE to the platform's superseded-isolate reset (a DO
|
|
747
|
+
* code-update reset): that is this Durable Object resetting over a container it
|
|
748
|
+
* did not touch, the container unchanged, so it is never a runtime replacement
|
|
749
|
+
* and never swaps the incarnation on the `/exec` path. `run()` answers it as a
|
|
750
|
+
* `control-reset` (`isControlReset`), and the restore path folds it in
|
|
751
|
+
* explicitly at its own site (`isRuntimeReplacement(err) || isControlReset(err)`)
|
|
752
|
+
* — the ONE place a DO reset shares the replacement disposition, since a reset
|
|
753
|
+
* under a live restore leaves nothing to land and re-restores onto the container
|
|
754
|
+
* that comes back. Keeping it out of this predicate makes that use the only one. */
|
|
760
755
|
function isRuntimeReplacement(err: unknown): boolean {
|
|
761
756
|
if (err instanceof StaleProcessHandleError) return true;
|
|
762
757
|
if (err instanceof RuntimeIdentityInactiveError) return true;
|
|
763
758
|
if (err instanceof OperationInterruptedError && RUNTIME_REPLACED_REASONS.has(err.reason)) return true;
|
|
764
759
|
if (err instanceof RPCTransportError && RPC_TRANSPORT_LOSS_KINDS.has(err.kind)) return true;
|
|
765
|
-
if (isDurableObjectCodeUpdateReset(err)) return true;
|
|
766
760
|
for (const link of selfAndCauses(err)) if (RUNTIME_REPLACEMENT_WORDING.test(errMsg(link))) return true;
|
|
767
761
|
return false;
|
|
768
762
|
}
|
|
@@ -771,32 +765,50 @@ function isRuntimeReplacement(err: unknown): boolean {
|
|
|
771
765
|
* — a new incarnation serves the thread, so the container the command was
|
|
772
766
|
* aimed at is gone whatever else the resident knows: the typed classes for
|
|
773
767
|
* a stale process handle and an inactive runtime identity, an interruption
|
|
774
|
-
* whose reason is a replaced runtime, the
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
768
|
+
* whose reason is a replaced runtime, and the SDK's wordings for the same
|
|
769
|
+
* (`RUNTIME_MOVED_WORDING`, on the error and its cause chain). NOT the words
|
|
770
|
+
* for a container that is merely down (`STOPPED_CONTAINER_WORDING`: "The
|
|
771
|
+
* container is not running", "Process supervisor is closed") and NOT a
|
|
772
|
+
* transport lost under the call (`RPCTransportError`): an asleep or starting
|
|
773
|
+
* container and a network blip answer those too, so on `/exec` they are the
|
|
774
|
+
* word only when the resident itself knows the container it held is gone
|
|
775
|
+
* (`replacedExecAnswer`). NOT the platform's superseded-isolate reset either:
|
|
776
|
+
* that is this Durable Object resetting over a container it did not touch —
|
|
777
|
+
* `run()` intercepts it first, in both phases, as a `control-reset`
|
|
778
|
+
* (`isControlReset`), so a DO-reset cause never reaches this predicate, and a
|
|
779
|
+
* vouch for it here would say the opposite of that word. The restore path
|
|
780
|
+
* keeps the union by naming both explicitly at its site
|
|
781
|
+
* (`isRuntimeReplacement(err) || isControlReset(err)`), so a step interrupted
|
|
782
|
+
* either way is retried onto the container that comes back — while this
|
|
783
|
+
* predicate and `isRuntimeReplacement` stay immune to the reset. */
|
|
784
784
|
function sdkVouchesRuntimeMoved(err: unknown): boolean {
|
|
785
785
|
if (err instanceof StaleProcessHandleError) return true;
|
|
786
786
|
if (err instanceof RuntimeIdentityInactiveError) return true;
|
|
787
787
|
if (err instanceof OperationInterruptedError && RUNTIME_REPLACED_REASONS.has(err.reason)) return true;
|
|
788
|
-
if (isDurableObjectCodeUpdateReset(err)) return true;
|
|
789
788
|
for (const link of selfAndCauses(err)) if (RUNTIME_MOVED_WORDING.test(errMsg(link))) return true;
|
|
790
789
|
return false;
|
|
791
790
|
}
|
|
792
791
|
|
|
793
|
-
/** The
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
|
|
799
|
-
|
|
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
|
+
});
|
|
803
|
+
|
|
804
|
+
/** Whether a command's failure is the resident's own Durable Object resetting
|
|
805
|
+
* under it (a Worker-code deploy) rather than the container runtime being
|
|
806
|
+
* replaced. Checked BEFORE `isRuntimeReplacement` on the thread `run()` path,
|
|
807
|
+
* so a DO reset over a still-running container answers `control-reset`, never
|
|
808
|
+
* the replaced word. The SDK's predicate already walks the cause
|
|
809
|
+
* chain (`selfAndCauses`), so it is called directly on the caught error. */
|
|
810
|
+
function isControlReset(err: unknown): boolean {
|
|
811
|
+
return isDurableObjectCodeUpdateReset(err);
|
|
800
812
|
}
|
|
801
813
|
|
|
802
814
|
/** The container's control port never answered: `exec` rejected with the
|
|
@@ -1122,22 +1134,6 @@ interface ThreadBinding {
|
|
|
1122
1134
|
depsKey?: string;
|
|
1123
1135
|
}
|
|
1124
1136
|
|
|
1125
|
-
/** Named, RPC-cloneable error shape for the thread data plane. The Worker
|
|
1126
|
-
* maps `status` to the HTTP status; extra fields (`needs`, `state`,
|
|
1127
|
-
* `reason`) ride along into the body. */
|
|
1128
|
-
interface ThreadErr {
|
|
1129
|
-
error: string;
|
|
1130
|
-
status: number;
|
|
1131
|
-
/** The steps the request ran before it failed (docs/reference/specs/tracing.md item 19):
|
|
1132
|
-
* a failed attach's trace is the one that says which step blew the budget. */
|
|
1133
|
-
trace?: ResidentStep[];
|
|
1134
|
-
needs?: string;
|
|
1135
|
-
/** With needs:"ref" — the resident's default branch, so the caller can bind by default. */
|
|
1136
|
-
defaultRef?: string;
|
|
1137
|
-
state?: ResidentState;
|
|
1138
|
-
reason?: string;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
1137
|
interface AttachOk {
|
|
1142
1138
|
workspace: string;
|
|
1143
1139
|
ref: string;
|
|
@@ -1618,49 +1614,49 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
1618
1614
|
* survives, so hydration state is always probed from disk. */
|
|
1619
1615
|
private hydration: Promise<void> | null = null;
|
|
1620
1616
|
|
|
1621
|
-
/**
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
*
|
|
1625
|
-
*
|
|
1626
|
-
*
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1632
|
-
*
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1617
|
+
/** Whether this resident knows the container it held is gone
|
|
1618
|
+
* (docs/reference/specs/resident-repos.md item 43): a restore under way for
|
|
1619
|
+
* this resident (`restoring`, persisted before any restore work) — its own
|
|
1620
|
+
* knowledge, reliable — and nothing else.
|
|
1621
|
+
*
|
|
1622
|
+
* Not the Container base's exit state, not a replayed `onStop`, not the
|
|
1623
|
+
* platform's `running` flag: each was tried or weighed and separates
|
|
1624
|
+
* nothing — a roll and an idle sleep leave the same traces, in both
|
|
1625
|
+
* directions — and the why, with the pinned library's lines, is the
|
|
1626
|
+
* record's alone (docs/reference/specs/resident-repos.md item 43), so a
|
|
1627
|
+
* dependency bump rots one place, not three. The harness's own probe
|
|
1628
|
+
* (`identity`, `alive`) stays the arbiter, so under-saying the word here is
|
|
1629
|
+
* safe and over-saying it is what orphans a process.
|
|
1630
|
+
*
|
|
1631
|
+
* A read that fails (the storage under a reset or a restore) is no
|
|
1632
|
+
* knowledge: the answer falls back to the SDK's words and the harness
|
|
1633
|
+
* probes, never an unhandled throw where a 409 was due. */
|
|
1634
|
+
private async knowsContainerGone(): Promise<boolean> {
|
|
1635
|
+
try {
|
|
1636
|
+
return (await this.getStatus()).state === "restoring";
|
|
1637
|
+
} catch {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1641
1640
|
}
|
|
1642
1641
|
|
|
1643
|
-
/**
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
private async knowsContainerGone(): Promise<boolean> {
|
|
1651
|
-
if (this.containerStop !== undefined) return true;
|
|
1652
|
-
return (await this.getStatus()).state === "restoring";
|
|
1642
|
+
/** The one decision on a failure the SDK classified as a runtime
|
|
1643
|
+
* replacement, made where it is classified (`run()`): the SDK vouched the
|
|
1644
|
+
* runtime moved, or a restore is under way for this resident. It gates the
|
|
1645
|
+
* word on `/exec` and the incarnation mint alike; the per-incarnation memos
|
|
1646
|
+
* clear before it, unconditionally. */
|
|
1647
|
+
private async replacementKnown(err: unknown): Promise<boolean> {
|
|
1648
|
+
return sdkVouchesRuntimeMoved(err) || (await this.knowsContainerGone());
|
|
1653
1649
|
}
|
|
1654
1650
|
|
|
1655
1651
|
/** The `/exec` answer for a command the SDK failed with a runtime
|
|
1656
1652
|
* replacement (item 43; harness.md item 6): the word — `reason:
|
|
1657
|
-
* "runtime-replaced"`, the resident's sentence — when the
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1662
|
-
private
|
|
1663
|
-
if (
|
|
1653
|
+
* "runtime-replaced"`, the resident's sentence — when the replacement was
|
|
1654
|
+
* known at the choke point (`RuntimeReplacedError.known`); otherwise what
|
|
1655
|
+
* the SDK said, with no word and no `reason`, so the harness's one more
|
|
1656
|
+
* command decides. The command is never re-issued either way: it may have
|
|
1657
|
+
* started. */
|
|
1658
|
+
private replacedExecAnswer(err: RuntimeReplacedError): ThreadErr {
|
|
1659
|
+
if (err.known) return runtimeReplacedErr(err);
|
|
1664
1660
|
return { error: errMsg(err.cause), status: 409 };
|
|
1665
1661
|
}
|
|
1666
1662
|
|
|
@@ -2024,6 +2020,12 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2024
2020
|
try {
|
|
2025
2021
|
proc = await createExtensionProcessSandbox(this).exec(argv as unknown as SandboxCommand, launch);
|
|
2026
2022
|
} catch (err) {
|
|
2023
|
+
// A DO code-update reset (our own Worker deploy) BEFORE a runtime
|
|
2024
|
+
// replacement: the container is unchanged, so this is `control-reset`, not
|
|
2025
|
+
// the replaced word — never `swapIncarnation`, and the command's outcome
|
|
2026
|
+
// is unknown. Checked first so a DO reset over a live pi is never
|
|
2027
|
+
// read as a replaced container.
|
|
2028
|
+
if (isControlReset(err)) throw new ControlResetError("spawn", err);
|
|
2027
2029
|
if (!isRuntimeReplacement(err)) {
|
|
2028
2030
|
// The control port never answered the SDK's connect (its 30 s abort,
|
|
2029
2031
|
// raised inside the wake path): no process started and nothing about
|
|
@@ -2041,8 +2043,25 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2041
2043
|
// takes the throw below. It exists so that if a future SDK vouches "never
|
|
2042
2044
|
// started" we retry then — and only then — without a change here.
|
|
2043
2045
|
if (!(err instanceof OperationInterruptedError && err.retryable === true)) {
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
+
// Two consequences with different safety, split here. The memos
|
|
2047
|
+
// (`hydratedVerdictAt`, `gitSetupDone`, `stageDirsReady`, the deps
|
|
2048
|
+
// flags — the fact that was stale) clear on EVERY replacement the SDK
|
|
2049
|
+
// classified: cheap to re-derive, and the per-incarnation memo block's
|
|
2050
|
+
// invariant is that this one choke point clears them, whatever the
|
|
2051
|
+
// word will say. The incarnation — the lease fencing token, one
|
|
2052
|
+
// isolate paired with one container runtime — is minted only when the
|
|
2053
|
+
// replacement is KNOWN: `isRuntimeReplacement` is the union, and a
|
|
2054
|
+
// transport blip on one request that minted an id would make
|
|
2055
|
+
// `takeMutex` read the live holder's row as `holder-incarnation-gone`
|
|
2056
|
+
// and hand the mirror mutex to a concurrent `/attach` over a running
|
|
2057
|
+
// fetch. (A swap gated whole on `known` was tried and left the memos
|
|
2058
|
+
// stale on a real roll, where `known` is false while the wake rewrites
|
|
2059
|
+
// the base's state — resident-repos item 43.) Only the word is gated,
|
|
2060
|
+
// on the same `known`.
|
|
2061
|
+
this.clearIncarnationMemos();
|
|
2062
|
+
const known = await this.replacementKnown(err);
|
|
2063
|
+
if (known) this.swapIncarnation();
|
|
2064
|
+
throw new RuntimeReplacedError("spawn", err, known);
|
|
2046
2065
|
}
|
|
2047
2066
|
console.log(
|
|
2048
2067
|
`exec: runtime replaced before the process started (SDK says retryable) — retrying once: ${errMsg(err)}`,
|
|
@@ -2050,9 +2069,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2050
2069
|
proc = await createExtensionProcessSandbox(this).exec(argv as unknown as SandboxCommand, launch);
|
|
2051
2070
|
}
|
|
2052
2071
|
// The spawn is the proof the control port answers: a persisted count of
|
|
2053
|
-
// unanswered connects ends here, whatever the command goes on to do
|
|
2054
|
-
// a container stop seen before it is history (`knowsContainerGone`).
|
|
2055
|
-
this.containerStop = undefined;
|
|
2072
|
+
// unanswered connects ends here, whatever the command goes on to do.
|
|
2056
2073
|
await this.clearRuntimeUnreachable();
|
|
2057
2074
|
try {
|
|
2058
2075
|
const out = await proc.output({ encoding: "utf8", timeout: timeout + 30_000 });
|
|
@@ -2066,9 +2083,18 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2066
2083
|
truncated: out.truncated,
|
|
2067
2084
|
};
|
|
2068
2085
|
} catch (err) {
|
|
2086
|
+
// A DO code-update reset during collect: the container and the process are
|
|
2087
|
+
// unchanged (only this DO's isolate reset), so the outcome is unknown but
|
|
2088
|
+
// never a replacement — `control-reset`, no `swapIncarnation`.
|
|
2089
|
+
// Checked before the replacement branch below.
|
|
2090
|
+
if (isControlReset(err)) throw new ControlResetError("collect", err);
|
|
2069
2091
|
if (isRuntimeReplacement(err)) {
|
|
2070
|
-
|
|
2071
|
-
|
|
2092
|
+
// The same split as the spawn site above: the memos unconditionally,
|
|
2093
|
+
// the incarnation only when the replacement is known, the word gated.
|
|
2094
|
+
this.clearIncarnationMemos();
|
|
2095
|
+
const known = await this.replacementKnown(err);
|
|
2096
|
+
if (known) this.swapIncarnation();
|
|
2097
|
+
throw new RuntimeReplacedError("collect", err, known);
|
|
2072
2098
|
}
|
|
2073
2099
|
if (err instanceof ProcessWaitTimeoutError) {
|
|
2074
2100
|
// The supervisor should have killed the process at `timeout`; 30 s
|
|
@@ -2648,7 +2674,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2648
2674
|
// asks the runtime whether the container the extract wrote to is still
|
|
2649
2675
|
// there (the incident that named this: a resident went `down` on exactly
|
|
2650
2676
|
// this, 10 ms before every exec answered "container is not running").
|
|
2651
|
-
|
|
2677
|
+
// A DO code-update reset under a live restore leaves nothing to land (the
|
|
2678
|
+
// restore's SDK call is gone with this isolate), so it shares the
|
|
2679
|
+
// replacement disposition — interrupted, retried onto the container that
|
|
2680
|
+
// comes back. This is the ONLY site that folds the DO reset in;
|
|
2681
|
+
// `isRuntimeReplacement` itself stays immune to it, so `/exec` never
|
|
2682
|
+
// answers the replaced word for a reset over a live container.
|
|
2683
|
+
const runtimeReplaced = isRuntimeReplacement(err) || isControlReset(err);
|
|
2652
2684
|
const runtimeActive = runtimeReplaced ? false : await this.isRuntimeActive().catch(() => null);
|
|
2653
2685
|
const disposition = restoreFailureDisposition(errMsg(err), {
|
|
2654
2686
|
runtimeReplaced,
|
|
@@ -3301,6 +3333,14 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3301
3333
|
await this.recoverFromDiskFull(failure.reason, selfInFlight);
|
|
3302
3334
|
return { ok: false, reason: failure.reason };
|
|
3303
3335
|
}
|
|
3336
|
+
// Item 67: a fetch that failed on the MIRROR (its remote gone, its
|
|
3337
|
+
// object store broken) is the resident's own step failing — `fetch-failed`,
|
|
3338
|
+
// counted by the ladder, never serviceable — not GitHub being unreachable.
|
|
3339
|
+
// The first live row found a broken mirror parked as `github-unreachable`.
|
|
3340
|
+
if (fetchFailureIsMirrors(message)) {
|
|
3341
|
+
await this.refreshFailed(failure, selfInFlight);
|
|
3342
|
+
return { ok: false, reason: failure.reason };
|
|
3343
|
+
}
|
|
3304
3344
|
const reason = `github-unreachable: ${message}`;
|
|
3305
3345
|
await this.setResidentState("degraded", reason);
|
|
3306
3346
|
return { ok: false, reason };
|
|
@@ -4427,7 +4467,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4427
4467
|
const reason = diskPressureReason({ verdict: final, evicted, kept, spares });
|
|
4428
4468
|
console.log(`attach ${input.threadKey}: ${reason}`);
|
|
4429
4469
|
const s = await this.getStatus();
|
|
4430
|
-
return { error: reason, status: 503, state: s.state, reason: DISK_PRESSURE_REASON };
|
|
4470
|
+
return { error: reason, status: 503, state: s.state, stateReason: s.reason, reason: DISK_PRESSURE_REASON };
|
|
4431
4471
|
}
|
|
4432
4472
|
const m = final.math;
|
|
4433
4473
|
console.log(
|
|
@@ -4827,6 +4867,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4827
4867
|
error: "image-stale: the container predates the current pool and is restarting; retry shortly",
|
|
4828
4868
|
status: 503,
|
|
4829
4869
|
state: "restoring",
|
|
4870
|
+
stateReason: "",
|
|
4830
4871
|
reason: "image-stale",
|
|
4831
4872
|
};
|
|
4832
4873
|
}
|
|
@@ -4854,7 +4895,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4854
4895
|
this.attachesInFlight--;
|
|
4855
4896
|
}
|
|
4856
4897
|
} catch (err) {
|
|
4857
|
-
return
|
|
4898
|
+
return catchAllErr(err, "attach-failed");
|
|
4858
4899
|
}
|
|
4859
4900
|
}
|
|
4860
4901
|
|
|
@@ -4866,7 +4907,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4866
4907
|
* instance's entry gate (the cron's, within one bucket) — an attach never
|
|
4867
4908
|
* stops the container itself: it is in flight. */
|
|
4868
4909
|
private async attachFailed(err: unknown): Promise<ThreadErr> {
|
|
4869
|
-
if (!(err instanceof StepError)) return
|
|
4910
|
+
if (!(err instanceof StepError)) return catchAllErr(err, "attach-failed");
|
|
4870
4911
|
const failure = await this.classifyFailure(err.step, err.message);
|
|
4871
4912
|
if (!failure.diskFull) return { error: `attach-failed at ${err.step}: ${err.message}`, status: 500 };
|
|
4872
4913
|
console.log(`attach: ${failure.reason}`);
|
|
@@ -4890,7 +4931,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4890
4931
|
await this.refreshIfStale(resourceId);
|
|
4891
4932
|
} catch (err) {
|
|
4892
4933
|
const s = await this.getStatus();
|
|
4893
|
-
return {
|
|
4934
|
+
return {
|
|
4935
|
+
error: `not-serviceable: ${errMsg(err)}`,
|
|
4936
|
+
status: 503,
|
|
4937
|
+
state: s.state,
|
|
4938
|
+
stateReason: s.reason,
|
|
4939
|
+
reason: s.reason,
|
|
4940
|
+
};
|
|
4894
4941
|
}
|
|
4895
4942
|
// `resourceId` was read by the caller a moment ago (item 15 of the audit:
|
|
4896
4943
|
// this used to re-read the same key), the registry record rides in from
|
|
@@ -4902,7 +4949,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4902
4949
|
const stored = await this.ctx.storage.get<RepoFacts | ThreadBinding>([FACTS_KEY, threadBindingKey(threadKey)]);
|
|
4903
4950
|
const facts = stored.get(FACTS_KEY) as RepoFacts | undefined;
|
|
4904
4951
|
const record = recordFromRoute ?? (await this.registry().getRecord(resource));
|
|
4905
|
-
|
|
4952
|
+
// Typed `reason` beside the words: the client reads the field — a refusal no
|
|
4953
|
+
// wait clears, unlike the restore window's 503s — never the sentence.
|
|
4954
|
+
if (!record || !facts)
|
|
4955
|
+
return { error: "not-serviceable: registry record or repo facts missing", status: 503, reason: "unregistered" };
|
|
4906
4956
|
|
|
4907
4957
|
// The binding's ref wins for the thread's whole life, with one exception
|
|
4908
4958
|
// (item 16): a thread bound to the repo default for want of a named branch
|
|
@@ -5052,9 +5102,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5052
5102
|
} catch (err) {
|
|
5053
5103
|
if (err instanceof MirrorBusyError) {
|
|
5054
5104
|
const s = await this.getStatus();
|
|
5055
|
-
return { error: errMsg(err), status: 503, state: s.state, reason: "mirror-busy" };
|
|
5105
|
+
return { error: errMsg(err), status: 503, state: s.state, stateReason: s.reason, reason: "mirror-busy" };
|
|
5056
5106
|
}
|
|
5057
|
-
return
|
|
5107
|
+
return catchAllErr(err, "attach-failed");
|
|
5058
5108
|
}
|
|
5059
5109
|
if (outcome.kind === "none") return { binding: outcome.binding };
|
|
5060
5110
|
if (outcome.kind === "refuse") {
|
|
@@ -5354,7 +5404,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5354
5404
|
return { error: `reuse-refused: ${err.why}`, status: 409, needs: "recreate" };
|
|
5355
5405
|
if (err instanceof MirrorBusyError) {
|
|
5356
5406
|
const s = await this.getStatus();
|
|
5357
|
-
return { error: errMsg(err), status: 503, state: s.state, reason: "mirror-busy" };
|
|
5407
|
+
return { error: errMsg(err), status: 503, state: s.state, stateReason: s.reason, reason: "mirror-busy" };
|
|
5358
5408
|
}
|
|
5359
5409
|
if (err instanceof StepError && err.step === "unknown-ref") {
|
|
5360
5410
|
return { error: `unknown-ref: ${err.message}`, status: 400 };
|
|
@@ -5399,7 +5449,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5399
5449
|
// worktree lock above, so the bot-side fallback can retry.
|
|
5400
5450
|
if (err instanceof MirrorBusyError) {
|
|
5401
5451
|
const s = await this.getStatus();
|
|
5402
|
-
return { error: errMsg(err), status: 503, state: s.state, reason: "mirror-busy" };
|
|
5452
|
+
return { error: errMsg(err), status: 503, state: s.state, stateReason: s.reason, reason: "mirror-busy" };
|
|
5403
5453
|
}
|
|
5404
5454
|
return this.attachFailed(err);
|
|
5405
5455
|
}
|
|
@@ -6237,7 +6287,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6237
6287
|
await this.ensureHydrated();
|
|
6238
6288
|
} catch (err) {
|
|
6239
6289
|
const s = await this.getStatus();
|
|
6240
|
-
return {
|
|
6290
|
+
return {
|
|
6291
|
+
error: `not-serviceable: ${errMsg(err)}`,
|
|
6292
|
+
status: 503,
|
|
6293
|
+
state: s.state,
|
|
6294
|
+
stateReason: s.reason,
|
|
6295
|
+
reason: s.reason,
|
|
6296
|
+
};
|
|
6241
6297
|
}
|
|
6242
6298
|
const binding = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(threadKey));
|
|
6243
6299
|
if (!binding) {
|
|
@@ -6321,6 +6377,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6321
6377
|
try {
|
|
6322
6378
|
return await this.execThreadBody(threadKey, command, timeoutMs, env);
|
|
6323
6379
|
} catch (err) {
|
|
6380
|
+
// A DO reset over a live container answers its own word at once — the
|
|
6381
|
+
// container is unchanged, so no `replacedExecAnswer` gate applies.
|
|
6382
|
+
if (err instanceof ControlResetError) return controlResetErr(err);
|
|
6324
6383
|
if (err instanceof RuntimeReplacedError) return this.replacedExecAnswer(err);
|
|
6325
6384
|
throw err;
|
|
6326
6385
|
}
|
|
@@ -6346,7 +6405,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6346
6405
|
: {}),
|
|
6347
6406
|
} satisfies ThreadBinding);
|
|
6348
6407
|
|
|
6349
|
-
// A runtime replacement under the command propagates to
|
|
6408
|
+
// A runtime replacement or a control reset under the command propagates to
|
|
6409
|
+
// `execThread`'s gate (`replacedExecAnswer` / `controlResetErr`).
|
|
6350
6410
|
const r = await this.threadRunCapped(binding.user, binding.worktreePath, command, timeoutMs, EXEC_OUTPUT_CAP, env);
|
|
6351
6411
|
const truncated = r.stdout.length > EXEC_OUTPUT_CAP || r.stderr.length > EXEC_OUTPUT_CAP || r.truncated === true;
|
|
6352
6412
|
const notes: string[] = [];
|
|
@@ -6399,6 +6459,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6399
6459
|
capBytesFor(READ_CONTENT_CAP),
|
|
6400
6460
|
);
|
|
6401
6461
|
} catch (err) {
|
|
6462
|
+
if (err instanceof ControlResetError) return controlResetErr(err);
|
|
6402
6463
|
if (err instanceof RuntimeReplacedError) return runtimeReplacedErr(err);
|
|
6403
6464
|
throw err;
|
|
6404
6465
|
}
|
|
@@ -6447,6 +6508,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6447
6508
|
}
|
|
6448
6509
|
return { encoding: "base64", content: parts.join(""), size };
|
|
6449
6510
|
} catch (err) {
|
|
6511
|
+
if (err instanceof ControlResetError) return controlResetErr(err);
|
|
6450
6512
|
if (err instanceof RuntimeReplacedError) return runtimeReplacedErr(err);
|
|
6451
6513
|
throw err;
|
|
6452
6514
|
}
|
|
@@ -6493,6 +6555,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6493
6555
|
DEFAULT_EXEC_TIMEOUT_MS,
|
|
6494
6556
|
);
|
|
6495
6557
|
} catch (err) {
|
|
6558
|
+
if (err instanceof ControlResetError) return controlResetErr(err);
|
|
6496
6559
|
if (err instanceof RuntimeReplacedError) return runtimeReplacedErr(err);
|
|
6497
6560
|
const step = err instanceof StepError ? ` at ${err.step}` : "";
|
|
6498
6561
|
return { error: `write-failed${step}: ${errMsg(err)}`, status: 400 };
|
|
@@ -6859,7 +6922,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6859
6922
|
await this.ensureHydrated();
|
|
6860
6923
|
} catch (err) {
|
|
6861
6924
|
const s = await this.getStatus();
|
|
6862
|
-
return {
|
|
6925
|
+
return {
|
|
6926
|
+
error: `not-serviceable: ${errMsg(err)}`,
|
|
6927
|
+
status: 503,
|
|
6928
|
+
state: s.state,
|
|
6929
|
+
stateReason: s.reason,
|
|
6930
|
+
reason: s.reason,
|
|
6931
|
+
};
|
|
6863
6932
|
}
|
|
6864
6933
|
// One storage round trip for the two facts; the registry lookup stays (an
|
|
6865
6934
|
// op resolves ONLY through the onboard-time command table).
|
|
@@ -6867,7 +6936,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6867
6936
|
const resource = (stored.get(RESOURCE_KEY) as string | undefined) ?? "";
|
|
6868
6937
|
const facts = stored.get(FACTS_KEY) as RepoFacts | undefined;
|
|
6869
6938
|
const record = await this.registry().getRecord(resource);
|
|
6870
|
-
|
|
6939
|
+
// Typed `reason` beside the words: the client reads the field — a refusal no
|
|
6940
|
+
// wait clears, unlike the restore window's 503s — never the sentence.
|
|
6941
|
+
if (!record || !facts)
|
|
6942
|
+
return { error: "not-serviceable: registry record or repo facts missing", status: 503, reason: "unregistered" };
|
|
6871
6943
|
const command = record.commands[op];
|
|
6872
6944
|
if (!command) return { error: `op-unavailable: the command table has no "${op}" entry`, status: 400 };
|
|
6873
6945
|
|
|
@@ -6963,13 +7035,15 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6963
7035
|
} catch (err) {
|
|
6964
7036
|
if (err instanceof MirrorBusyError) {
|
|
6965
7037
|
const s = await this.getStatus();
|
|
6966
|
-
return { error: errMsg(err), status: 503, state: s.state, reason: "mirror-busy" };
|
|
7038
|
+
return { error: errMsg(err), status: 503, state: s.state, stateReason: s.reason, reason: "mirror-busy" };
|
|
6967
7039
|
}
|
|
6968
7040
|
if (err instanceof StepError && err.step === "unknown-ref") {
|
|
6969
7041
|
return { error: `unknown-ref: ${err.message}`, status: 400 };
|
|
6970
7042
|
}
|
|
6971
|
-
|
|
6972
|
-
|
|
7043
|
+
// A step that failed is named and deterministic; a throw no step named is
|
|
7044
|
+
// typed by the one builder every such 500 goes through.
|
|
7045
|
+
if (err instanceof StepError) return { error: `op-failed at ${err.step}: ${errMsg(err)}`, status: 500 };
|
|
7046
|
+
return catchAllErr(err, "op-failed");
|
|
6973
7047
|
} finally {
|
|
6974
7048
|
// Disposable means disposable: the checkout dies with the op, pass or
|
|
6975
7049
|
// fail (best effort — a slept container already destroyed it anyway).
|
|
@@ -7123,13 +7197,22 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7123
7197
|
fates.set(ref, { fate: looked.fate, detail });
|
|
7124
7198
|
}),
|
|
7125
7199
|
);
|
|
7200
|
+
// The runs registered on this resident (item 44): a run holds its tree
|
|
7201
|
+
// from its attach to its release, and its process lives in the container
|
|
7202
|
+
// between the bot's calls — the op counters read 0 while its model thinks.
|
|
7203
|
+
const registered = new Set(
|
|
7204
|
+
[...(await this.ctx.storage.list<RunRegistration>({ prefix: RUN_REG_KEY_PREFIX })).values()].map(
|
|
7205
|
+
(r) => r.threadKey,
|
|
7206
|
+
),
|
|
7207
|
+
);
|
|
7126
7208
|
for (const binding of live) {
|
|
7127
7209
|
const isDefaultRef = binding.ref === defaultRef;
|
|
7128
7210
|
// The default branch is never a finished ref (reclaimDecision keeps it
|
|
7129
7211
|
// by name), so its fate is never looked up.
|
|
7130
7212
|
const { fate, detail } = (!isDefaultRef && fates.get(binding.ref)) || { fate: "unknown" as RefFate, detail: "" };
|
|
7131
7213
|
const busy = this.threadOpsInFlight.get(binding.threadKey) ?? 0;
|
|
7132
|
-
const
|
|
7214
|
+
const held = registered.has(binding.threadKey);
|
|
7215
|
+
const decision = reclaimDecision({ fate, isDefaultRef, busy, held });
|
|
7133
7216
|
if (!decision.reclaim) {
|
|
7134
7217
|
kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: decision.why });
|
|
7135
7218
|
continue;
|
|
@@ -7146,6 +7229,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7146
7229
|
kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: "busy" });
|
|
7147
7230
|
continue;
|
|
7148
7231
|
}
|
|
7232
|
+
// A run attached while the tree was measured holds it now (item 44).
|
|
7233
|
+
if ((await this.ctx.storage.get<RunRegistration>(runRegKey(binding.threadKey))) !== undefined) {
|
|
7234
|
+
kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: "run-held" });
|
|
7235
|
+
continue;
|
|
7236
|
+
}
|
|
7149
7237
|
if (!current || current.evicted || current.lastAttachAt !== binding.lastAttachAt) {
|
|
7150
7238
|
kept.push({ threadKey: binding.threadKey, ref: binding.ref, why: "re-attached" });
|
|
7151
7239
|
continue;
|
|
@@ -8052,7 +8140,7 @@ export default {
|
|
|
8052
8140
|
return json({ error: "unknown route" }, 404);
|
|
8053
8141
|
}
|
|
8054
8142
|
} catch (err) {
|
|
8055
|
-
return json(
|
|
8143
|
+
return json(catchAllErr(err), 500);
|
|
8056
8144
|
}
|
|
8057
8145
|
})();
|
|
8058
8146
|
root?.end(res.status >= 500 ? "error" : "ok", { httpStatus: res.status });
|
|
@@ -8602,7 +8690,7 @@ async function handleAttach(env: Env, body: Record<string, unknown>, traceparent
|
|
|
8602
8690
|
reason,
|
|
8603
8691
|
),
|
|
8604
8692
|
(result) => result,
|
|
8605
|
-
(err) => (
|
|
8693
|
+
(err) => catchAllErr(err),
|
|
8606
8694
|
);
|
|
8607
8695
|
}
|
|
8608
8696
|
|
|
@@ -8620,7 +8708,7 @@ async function handleAwaitRestore(env: Env, body: Record<string, unknown>): Prom
|
|
|
8620
8708
|
return streamHeartbeatJson(
|
|
8621
8709
|
residentStub(env, resource.resource).awaitRestore(),
|
|
8622
8710
|
(result) => result,
|
|
8623
|
-
(err) => (
|
|
8711
|
+
(err) => catchAllErr(err),
|
|
8624
8712
|
);
|
|
8625
8713
|
}
|
|
8626
8714
|
|
|
@@ -8698,33 +8786,23 @@ function streamHeartbeatJson<T>(
|
|
|
8698
8786
|
return new Response(stream, { headers: { "content-type": "application/json" } });
|
|
8699
8787
|
}
|
|
8700
8788
|
|
|
8701
|
-
/** /exec's payload mapping: a result as {stdout, stderr, exitCode, truncated}
|
|
8702
|
-
* a
|
|
8703
|
-
*
|
|
8704
|
-
*
|
|
8789
|
+
/** /exec's payload mapping: a result as {stdout, stderr, exitCode, truncated};
|
|
8790
|
+
* a failure as `execFailureDocument` — the one the Durable Object named, or
|
|
8791
|
+
* the pending result's rejection answered as the DO would have
|
|
8792
|
+
* (`threadRejectionErr`: a code-update reset the DO's own `control-reset`
|
|
8793
|
+
* word, a replacement the SDK's words on a bare 409 — the word withheld on
|
|
8794
|
+
* /exec alone —, anything else the typed 500), so a rejection streams its
|
|
8795
|
+
* `status`, its word and its `transient`,
|
|
8796
|
+
* and the client reads it like the JSON routes' answer, never as a
|
|
8797
|
+
* deterministic answer over HTTP 200. */
|
|
8705
8798
|
function streamThreadExec(pending: Promise<Awaited<ReturnType<ResidentDO["execThread"]>>>): Response {
|
|
8706
8799
|
return streamHeartbeatJson(
|
|
8707
8800
|
pending,
|
|
8708
8801
|
(result) =>
|
|
8709
8802
|
"error" in result
|
|
8710
|
-
?
|
|
8711
|
-
error: result.error,
|
|
8712
|
-
...(result.needs ? { needs: result.needs } : {}),
|
|
8713
|
-
// `reason` must stay independent of `state`: runtimeReplacedErr()
|
|
8714
|
-
// sets reason:"runtime-replaced" with NO state, and the client's
|
|
8715
|
-
// deploy-vs-dead-transport check reads it. Folding these two spreads
|
|
8716
|
-
// back into one silently drops it (no test covers this Worker).
|
|
8717
|
-
...(result.state ? { state: result.state } : {}),
|
|
8718
|
-
...(result.reason ? { reason: result.reason } : {}),
|
|
8719
|
-
stdout: "",
|
|
8720
|
-
stderr: result.error,
|
|
8721
|
-
exitCode: 127,
|
|
8722
|
-
}
|
|
8803
|
+
? execFailureDocument(result)
|
|
8723
8804
|
: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, truncated: result.truncated },
|
|
8724
|
-
(err) =>
|
|
8725
|
-
const msg = errMsg(err);
|
|
8726
|
-
return { error: msg, stdout: "", stderr: msg, exitCode: 127 };
|
|
8727
|
-
},
|
|
8805
|
+
(err) => execFailureDocument(threadRejectionErr(err, "/exec")),
|
|
8728
8806
|
);
|
|
8729
8807
|
}
|
|
8730
8808
|
|
|
@@ -8735,7 +8813,12 @@ async function handleRead(env: Env, body: Record<string, unknown>): Promise<Resp
|
|
|
8735
8813
|
return json({ error: "path must be a string relative to the thread worktree" }, 400);
|
|
8736
8814
|
const encoding = readEncodingOf(body);
|
|
8737
8815
|
if (typeof encoding !== "string") return json({ error: encoding.error }, 400);
|
|
8738
|
-
|
|
8816
|
+
// A stub that rejects (the DO reset under the call, a storage operation that
|
|
8817
|
+
// did not complete) is answered as the DO answers the same fact inside, never
|
|
8818
|
+
// left to the fetch handler's catch-all.
|
|
8819
|
+
const result = await ctx.stub
|
|
8820
|
+
.readThreadFile(ctx.threadKey, body.path, encoding)
|
|
8821
|
+
.catch((err: unknown) => threadRejectionErr(err, "/read"));
|
|
8739
8822
|
if ("error" in result) return threadErrResponse(result);
|
|
8740
8823
|
return json(result);
|
|
8741
8824
|
}
|
|
@@ -8748,7 +8831,9 @@ async function handleWrite(env: Env, body: Record<string, unknown>): Promise<Res
|
|
|
8748
8831
|
if (typeof body.content !== "string" || body.content.length > MAX_WRITE_CONTENT) {
|
|
8749
8832
|
return json({ error: `content must be a string of at most ${MAX_WRITE_CONTENT} chars` }, 400);
|
|
8750
8833
|
}
|
|
8751
|
-
const result = await ctx.stub
|
|
8834
|
+
const result = await ctx.stub
|
|
8835
|
+
.writeThreadFile(ctx.threadKey, body.path, body.content)
|
|
8836
|
+
.catch((err: unknown) => threadRejectionErr(err, "/write"));
|
|
8752
8837
|
if ("error" in result) return threadErrResponse(result);
|
|
8753
8838
|
return json(result);
|
|
8754
8839
|
}
|
|
@@ -8822,18 +8907,18 @@ async function handleOp(env: Env, body: Record<string, unknown>, traceparent?: s
|
|
|
8822
8907
|
}
|
|
8823
8908
|
|
|
8824
8909
|
/** /op's payload mapping: results pass through; a named error sheds its
|
|
8825
|
-
* transport-only `status` field (the body is the contract, never the code)
|
|
8910
|
+
* transport-only `status` field (the body is the contract, never the code); a
|
|
8911
|
+
* pending op that rejected is the typed 500 (`catchAllErr`, so `transient`
|
|
8912
|
+
* rides beside the words), its status shed the same way. */
|
|
8826
8913
|
function streamOp(pending: Promise<Awaited<ReturnType<ResidentDO["runOp"]>>>): Response {
|
|
8914
|
+
const shedStatus = (failure: ThreadErr): object => {
|
|
8915
|
+
const { status: _status, ...rest } = failure;
|
|
8916
|
+
return rest;
|
|
8917
|
+
};
|
|
8827
8918
|
return streamHeartbeatJson(
|
|
8828
8919
|
pending,
|
|
8829
|
-
(result) =>
|
|
8830
|
-
|
|
8831
|
-
const { status: _status, ...rest } = result;
|
|
8832
|
-
return rest;
|
|
8833
|
-
}
|
|
8834
|
-
return result;
|
|
8835
|
-
},
|
|
8836
|
-
(err) => ({ error: errMsg(err) }),
|
|
8920
|
+
(result) => ("error" in result ? shedStatus(result) : result),
|
|
8921
|
+
(err) => shedStatus(catchAllErr(err, "op-failed")),
|
|
8837
8922
|
);
|
|
8838
8923
|
}
|
|
8839
8924
|
|