@coreplane/switchboard 1.241.0 → 1.243.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 +45 -13
- package/dist/assets/deploy/cloudflare-memory/worker.ts +86 -29
- package/dist/assets/deploy/cloudflare-resident/worker.ts +298 -24
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +38 -5
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +5 -5
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +61 -48
- package/dist/assets/src/config/profile.ts +68 -3
- package/dist/assets/src/core/authz/actor.ts +12 -9
- package/dist/assets/src/core/authz/authorize.ts +17 -4
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/types.ts +12 -0
- package/dist/assets/src/core/budgets.ts +313 -0
- package/dist/assets/src/core/coordinator/contract.ts +3 -1
- package/dist/assets/src/core/coordinator/driver.ts +0 -10
- package/dist/assets/src/core/costs.ts +21 -80
- package/dist/assets/src/core/harness/scope.ts +21 -0
- 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 +48 -5
- package/dist/assets/src/core/runFriction.ts +2 -1
- package/dist/assets/src/core/runLedger/types.ts +2 -0
- package/dist/assets/src/core/runRecord.ts +51 -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 +4 -2
- package/dist/assets/src/core/ship/coordinator.ts +101 -84
- package/dist/assets/src/core/ship/handoff.ts +9 -0
- package/dist/assets/src/execution/bashTimeout.ts +8 -5
- package/dist/assets/src/execution/residentAutoRebuild.ts +4 -2
- package/dist/assets/src/execution/residentHead.ts +23 -10
- package/dist/assets/src/execution/residentInfraStreak.ts +89 -0
- package/dist/assets/src/execution/residentRefresh.ts +43 -4
- package/dist/assets/src/execution/residentSteps.ts +1 -0
- package/dist/assets/src/execution/sandboxErrors.ts +6 -0
- package/dist/assets/src/execution/seedPlan.ts +17 -0
- package/dist/assets/web/dist/.vite/manifest.json +443 -446
- package/dist/assets/web/dist/assets/{AppShell-DEy5jRuS.js → AppShell-IDMGG6yi.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +2 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-BGpPr5xr.js → DeliveryPage-n1tz5I_v.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DtmQM2gD.js → NotFoundPage-C97EhJcQ.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-C-48fxat.js → ResidentDetailPage-DACalNVF.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-CtNGNnhN.js → RunsTabs-BUfdk0lH.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-DPWE1JmO.js → StatusDot-Dw0T1M-P.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DKhSRH9t.js → Tooltip-BbLuIAiS.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-DgSK1qvr.js → angular-html-oBNfPJR0.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-D3gNdiSG.js → angular-ts-BvNwsyWA.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CkHCYM8I.js → apl-CNUdRlYf.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DLm45axt.js → astro-Zb0NriSe.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-BKa-VE-c.js → blade-qPRVheqq.js} +1 -1
- package/dist/assets/web/dist/assets/{c-C8NCNWai.js → c-D8Awx4YO.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DqDQ7IKH.js → chapel-Bt72Mhsx.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-j4vCD6AJ.js → cobol-BOBacexg.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-C6oDVH-0.js → coffee-E4u0liHW.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-Bd3A1Rtc.js → cpp-q2sLNlul.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CUOgOw0d.js → crystal-DyWqUnlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-RYljyv7G.js → css-CQY0hFsD.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BnJXKHCY.js → dist-twkFmSUY.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-BobbycC-.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BJhNdQDc.js → edge-C1MwhJkX.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-g1AWOAfT.js → elixir-Bb3YbHfn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-CER5e4Dz.js → elm-DAN9IGQw.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-D5aOmMQf.js → erb-BEB8Xlsj.js} +1 -1
- package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-D6rfV8jp.js → git-rebase-tqpjRfxO.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-BvDH3-mC.js → glimmer-js-Ccbo65zR.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-DChtNs9V.js → glimmer-ts-B6WBVMpE.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-BHlsWrxf.js → glsl-tRec3Fcu.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-BsaBHb53.js → graphql-P8kbxT4F.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-0lbDosCd.js → hack-H9Zhkagy.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-BKtco2vr.js → haml-DtEnpn7Z.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-BE_cj04z.js → handlebars-DkgPfoAz.js} +1 -1
- package/dist/assets/web/dist/assets/{html-BNcr8EVd.js → html-D30RXpIs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-BsJt2Kei.js → html-derivative-DwozLrEx.js} +1 -1
- package/dist/assets/web/dist/assets/{http-BR5P8ER_.js → http-DXuzBAPm.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CtHalpWS.js → hurl-d1UUJIt_.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-B_s5tKyq.js +1 -0
- package/dist/assets/web/dist/assets/{java-BXVdhN11.js → java-DL0gWf34.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CyoShgNQ.js → javascript-Cl7vavnS.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-BQYZzHex.js → jinja-CfQOWMX9.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DqrUa1Eq.js → jison-KdYirlqm.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CQiTF9Jj.js → json-C9cDQ-Qj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-Y16MkCYQ.js → jsx-CRx5NItd.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-zn16YONe.js → julia-CmsQsZQl.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DcVeN_MN.js → just-CsM3Q8TE.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BNNoF-WP.js → latex-BXCh5YRX.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-DAi6qHzn.js → liquid-BF2vwK8p.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-BswS0axw.js → lua-DcMBATrl.js} +1 -1
- package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +1 -0
- package/dist/assets/web/dist/assets/{main-B3is4sk6.js → main-d-w-tIKt.js} +2 -2
- package/dist/assets/web/dist/assets/{marko-C-4hkmQZ.js → marko-88MndKvG.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-0ZaHUkS7.js → mdc-DJ4kVAd8.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-DBDg5tOR.js → nginx-CP6mRgtV.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BVMz49Yw.js → nim-QQfj3fpF.js} +1 -1
- package/dist/assets/web/dist/assets/{org-1hO_K6ya.js → org-Bke3eBzc.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-CVRQLvVk.js → perl-Bkh0N7Kz.js} +1 -1
- package/dist/assets/web/dist/assets/{php-K9nCrB9n.js → php-Dnv1Piya.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-CTxOcmO3.js → pug-D6peFFI7.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CmCkfG5q.js → qml-D8PEs-C-.js} +1 -1
- package/dist/assets/web/dist/assets/{r-pP47Xn1X.js → r-B1EL9b_j.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BpW6r3tC.js → razor-BsMTIh2b.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CINgdY4N.js → regexp-HhvC8spD.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-BZIUEq2Q.js → rst-D5paAxpg.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-nzAOxz6r.js → ruby-BrQwhLrl.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-QIS1bFth.js → sas-afot2B1o.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-BuhBBVNt.js → scss-Efm-mwuG.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CQc1vXbk.js → shellscript-BK0Vv5fT.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CoubCAUv.js → shellsession-BGqlMC7N.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-CPRzlder.js → soy-M0b4UwGM.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-W9krb8-9.js → sql-sxe6IE9j.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-C9m_EB8J.js +9 -0
- package/dist/assets/web/dist/assets/{stata-CuISJEC0.js → stata-nPF_ddLP.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CSet7584.js → surrealql-D_GrC6u7.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-ChXTSYwK.js → svelte-DwL1AtNP.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-Bm55v62k.js → templ-7s7LTDkc.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-CICMX8Gj.js → tex-Bx-5fMxe.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-CA1UzWyB.js → ts-tags-DUMJnke_.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-Dy04HNbv.js → tsx-BN8biPDe.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-BBHsVnVD.js → twig-8nIu84TN.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-Dvc-wVBT.js → typescript-BooSPq_S.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Dvlqx3_q.js → typst-CTBiBsem.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-zWi1MNU2.js → vue-C6Ft4Lea.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-D4YxT3An.js → vue-html-Ba36dD5D.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-0YE7uQlH.js → vue-vine-NkFexVo2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-gm-iksZB.js → xml-C_THnHXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D_W5BcrY.js → xsl-D8G5xqjY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-CHmZ21wZ.js → yaml-jAMIJzge.js} +1 -1
- package/dist/cli.js +4121 -1902
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BuHD-Bv7.js +0 -2
- package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CBBIjPOa.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-Ci1Ygqnc.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +0 -1
- package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DEN5bSQf.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.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-DP_zSemY.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-yji8a1wM.js +0 -9
|
@@ -179,6 +179,7 @@ import {
|
|
|
179
179
|
isRuntimeUnreachableSignal,
|
|
180
180
|
killStaleBuildProcessesCommand,
|
|
181
181
|
planRefresh,
|
|
182
|
+
RUNTIME_MOVED_WORDING,
|
|
182
183
|
RUNTIME_REPLACEMENT_WORDING,
|
|
183
184
|
RUNTIME_UNREACHABLE_DOWN_AT,
|
|
184
185
|
runtimeUnreachableReason,
|
|
@@ -197,6 +198,14 @@ import {
|
|
|
197
198
|
type RefreshFailure,
|
|
198
199
|
} from "../../src/execution/residentRefresh.js";
|
|
199
200
|
import { autoRebuildDecision, isAutoRebuildEligible } from "../../src/execution/residentAutoRebuild.js";
|
|
201
|
+
import {
|
|
202
|
+
INFRA_STREAK_DOWN_AT,
|
|
203
|
+
infraStreakReason,
|
|
204
|
+
infraStreakRung,
|
|
205
|
+
type InfraStreakRow,
|
|
206
|
+
isRepoCommandStep,
|
|
207
|
+
parksOnRepeat,
|
|
208
|
+
} from "../../src/execution/residentInfraStreak.js";
|
|
200
209
|
import {
|
|
201
210
|
lifecycleOf,
|
|
202
211
|
parseLifecycle,
|
|
@@ -544,6 +553,8 @@ const GITHUB_API_TIMEOUT_MS = 10_000;
|
|
|
544
553
|
* idle-park like a warm one; the next attach still refreshes first. */
|
|
545
554
|
const DEGRADED_PARK_AFTER_CYCLES = 3;
|
|
546
555
|
const DEGRADED_STREAK_KEY = "resident:degradedStreak";
|
|
556
|
+
/** Consecutive cycles failed in the resident's OWN steps (item 67): the ladder's count, `InfraStreakRow`. */
|
|
557
|
+
const INFRA_STREAK_KEY = "resident:infraStreak";
|
|
547
558
|
/** Degraded reasons that are not evidence about the repository — both always
|
|
548
559
|
* carry a `: detail` suffix: the watchdog's `stale-mid-flight: …` (a marker
|
|
549
560
|
* a dead cycle left behind; a cycle must run) and the wake path's
|
|
@@ -561,7 +572,7 @@ const DEGRADED_STREAK_KEY = "resident:degradedStreak";
|
|
|
561
572
|
* re-enters the step. A control port that did not answer
|
|
562
573
|
* (`runtime-unreachable: …`, item 64) is the third: no command ran, the
|
|
563
574
|
* ladder over its own persisted count owns the recovery. */
|
|
564
|
-
const NON_EVIDENCE_REASON = /^(?:stale-mid-flight|restore-interrupted|runtime-unreachable):/;
|
|
575
|
+
const NON_EVIDENCE_REASON = /^(?:stale-mid-flight|restore-interrupted|runtime-unreachable|infra-streak):/;
|
|
565
576
|
/** When the disk-full recovery last stopped the container (docs/reference/specs/resident-repos.md item 54):
|
|
566
577
|
* feeds `planDiskFullRecovery`'s cooldown so a working set that refills the
|
|
567
578
|
* disk is named, not recycled in a loop. */
|
|
@@ -756,9 +767,34 @@ function isRuntimeReplacement(err: unknown): boolean {
|
|
|
756
767
|
return false;
|
|
757
768
|
}
|
|
758
769
|
|
|
770
|
+
/** Whether the SDK's failure vouches that the runtime under the command MOVED
|
|
771
|
+
* — a new incarnation serves the thread, so the container the command was
|
|
772
|
+
* aimed at is gone whatever else the resident knows: the typed classes for
|
|
773
|
+
* a stale process handle and an inactive runtime identity, an interruption
|
|
774
|
+
* whose reason is a replaced runtime, the platform's superseded-isolate
|
|
775
|
+
* reset, and the SDK's wordings for the same (`RUNTIME_MOVED_WORDING`, on the
|
|
776
|
+
* error and its cause chain). NOT the words for a container that is merely
|
|
777
|
+
* down (`STOPPED_CONTAINER_WORDING`: "The container is not running", "Process
|
|
778
|
+
* supervisor is closed") and NOT a transport lost under the call
|
|
779
|
+
* (`RPCTransportError`): an asleep or starting container and a network blip
|
|
780
|
+
* answer those too, so on `/exec` they are the word only when the resident
|
|
781
|
+
* itself knows the container it held is gone (`replacedExecAnswer`). The
|
|
782
|
+
* resident's own steps keep the union (`isRuntimeReplacement`): a step
|
|
783
|
+
* interrupted either way is retried onto the container that comes back. */
|
|
784
|
+
function sdkVouchesRuntimeMoved(err: unknown): boolean {
|
|
785
|
+
if (err instanceof StaleProcessHandleError) return true;
|
|
786
|
+
if (err instanceof RuntimeIdentityInactiveError) return true;
|
|
787
|
+
if (err instanceof OperationInterruptedError && RUNTIME_REPLACED_REASONS.has(err.reason)) return true;
|
|
788
|
+
if (isDurableObjectCodeUpdateReset(err)) return true;
|
|
789
|
+
for (const link of selfAndCauses(err)) if (RUNTIME_MOVED_WORDING.test(errMsg(link))) return true;
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
|
|
759
793
|
/** The named ThreadErr every thread route (exec/read/write) answers for a
|
|
760
794
|
* runtime replacement, so the client can classify it (409 like the other
|
|
761
|
-
* recoverable thread states; `reason` is the discriminator).
|
|
795
|
+
* recoverable thread states; `reason` is the discriminator). On `/exec` the
|
|
796
|
+
* answer goes through `replacedExecAnswer` first: the word only when the SDK
|
|
797
|
+
* vouched the runtime moved or the resident knows the container is gone. */
|
|
762
798
|
function runtimeReplacedErr(err: RuntimeReplacedError): ThreadErr {
|
|
763
799
|
return { error: err.message, status: 409, reason: "runtime-replaced" };
|
|
764
800
|
}
|
|
@@ -1479,6 +1515,17 @@ const threadBindingKey = (threadKey: string) => `${THREAD_KEY_PREFIX}${threadKey
|
|
|
1479
1515
|
|
|
1480
1516
|
const parentDir = (p: string): string => p.slice(0, p.lastIndexOf("/"));
|
|
1481
1517
|
|
|
1518
|
+
/** A run's registration, held from its attach to its release (item 44): a
|
|
1519
|
+
* harness run's process lives in the container BETWEEN the bot's operator
|
|
1520
|
+
* calls, so the in-memory op counters read 0 while it runs. Durable rows —
|
|
1521
|
+
* a fresh isolate still counts the process that survived in the container. */
|
|
1522
|
+
const RUN_REG_KEY_PREFIX = "runReg:";
|
|
1523
|
+
const runRegKey = (threadKey: string) => `${RUN_REG_KEY_PREFIX}${threadKey}`;
|
|
1524
|
+
interface RunRegistration {
|
|
1525
|
+
threadKey: string;
|
|
1526
|
+
registeredAt: string;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1482
1529
|
/** Deterministic per-thread+ref worktree path. Slugs replace anything outside
|
|
1483
1530
|
* [A-Za-z0-9._-]; a threadKey-derived hash suffix keeps two keys that slug
|
|
1484
1531
|
* identically (e.g. "a:b" vs "a-b") from colliding on disk. */
|
|
@@ -1571,6 +1618,52 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
1571
1618
|
* survives, so hydration state is always probed from disk. */
|
|
1572
1619
|
private hydration: Promise<void> | null = null;
|
|
1573
1620
|
|
|
1621
|
+
/** The container stop the platform delivered to `onStop` and no command has
|
|
1622
|
+
* answered since: the resident's own knowledge that the container it held
|
|
1623
|
+
* is gone (docs/reference/specs/resident-repos.md item 43). In memory on
|
|
1624
|
+
* purpose — a Durable Object that restarts learns the same fact from the
|
|
1625
|
+
* restore its next command's wake path starts (`restoring`), and a stop
|
|
1626
|
+
* recorded in storage would outlive the container that answers now. */
|
|
1627
|
+
private containerStop: { at: number; exitCode: number; reason: string } | undefined;
|
|
1628
|
+
|
|
1629
|
+
/** The platform's signal that the container stopped — the rollout's
|
|
1630
|
+
* `runtime_signal`, or an exit of its own — recorded before the SDK's
|
|
1631
|
+
* reconciliation runs, so an `/exec` that meets the stopped container from
|
|
1632
|
+
* here on answers `runtime-replaced` (`replacedExecAnswer`) rather than the
|
|
1633
|
+
* binding's bare "The container is not running". The Sandbox base declares
|
|
1634
|
+
* the hook without parameters; the Container base beneath it passes them. */
|
|
1635
|
+
async onStop(params?: { exitCode: number; reason: string }): Promise<void> {
|
|
1636
|
+
this.containerStop = { at: systemClock(), exitCode: params?.exitCode ?? 0, reason: params?.reason ?? "unknown" };
|
|
1637
|
+
console.log(
|
|
1638
|
+
`container: stopped (exit ${this.containerStop.exitCode}, ${this.containerStop.reason}) — the container this resident held is gone; /exec answers runtime-replaced until a command answers again`,
|
|
1639
|
+
);
|
|
1640
|
+
await super.onStop();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/** Whether this resident knows the container it held is gone: it saw the
|
|
1644
|
+
* container stop and no command has answered since, or a restore is under
|
|
1645
|
+
* way (`restoring`, persisted before any restore work). Never the
|
|
1646
|
+
* platform's `running` flag — an asleep or starting container answers
|
|
1647
|
+
* false to it too, and an `/exec` that meets one must say what the SDK
|
|
1648
|
+
* said so the harness probes rather than relaunch into a container that
|
|
1649
|
+
* was never replaced. */
|
|
1650
|
+
private async knowsContainerGone(): Promise<boolean> {
|
|
1651
|
+
if (this.containerStop !== undefined) return true;
|
|
1652
|
+
return (await this.getStatus()).state === "restoring";
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/** The `/exec` answer for a command the SDK failed with a runtime
|
|
1656
|
+
* replacement (item 43; harness.md item 6): the word — `reason:
|
|
1657
|
+
* "runtime-replaced"`, the resident's sentence — when the SDK vouched the
|
|
1658
|
+
* runtime moved or the resident knows the container it held is gone;
|
|
1659
|
+
* otherwise what the SDK said, with no word and no `reason`, so the
|
|
1660
|
+
* harness's one more command decides. The command is never re-issued
|
|
1661
|
+
* either way: it may have started. */
|
|
1662
|
+
private async replacedExecAnswer(err: RuntimeReplacedError): Promise<ThreadErr> {
|
|
1663
|
+
if (sdkVouchesRuntimeMoved(err.cause) || (await this.knowsContainerGone())) return runtimeReplacedErr(err);
|
|
1664
|
+
return { error: errMsg(err.cause), status: 409 };
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1574
1667
|
/** The step trace of the request in flight (docs/reference/specs/tracing.md item 19):
|
|
1575
1668
|
* `attachThread` and `runOp` each run inside their own collector, so the
|
|
1576
1669
|
* commands `runOk` runs and the mirror-lock waits land on the answer that
|
|
@@ -1957,7 +2050,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
1957
2050
|
proc = await createExtensionProcessSandbox(this).exec(argv as unknown as SandboxCommand, launch);
|
|
1958
2051
|
}
|
|
1959
2052
|
// The spawn is the proof the control port answers: a persisted count of
|
|
1960
|
-
// unanswered connects ends here, whatever the command goes on to do
|
|
2053
|
+
// unanswered connects ends here, whatever the command goes on to do — and
|
|
2054
|
+
// a container stop seen before it is history (`knowsContainerGone`).
|
|
2055
|
+
this.containerStop = undefined;
|
|
1961
2056
|
await this.clearRuntimeUnreachable();
|
|
1962
2057
|
try {
|
|
1963
2058
|
const out = await proc.output({ encoding: "utf8", timeout: timeout + 30_000 });
|
|
@@ -3099,10 +3194,12 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3099
3194
|
}
|
|
3100
3195
|
}
|
|
3101
3196
|
let settled = entry.state === "warm";
|
|
3102
|
-
if (entry.state === "degraded" && !isNonEvidenceReason(entry.reason)) {
|
|
3197
|
+
if (entry.state === "degraded" && !isNonEvidenceReason(entry.reason) && parksOnRepeat(entry.reason)) {
|
|
3103
3198
|
// Count consecutive cycles that found the same REFRESH-PRODUCED degraded
|
|
3104
|
-
// reason (github-unreachable,
|
|
3105
|
-
//
|
|
3199
|
+
// reason that is evidence about the repo or GitHub (github-unreachable,
|
|
3200
|
+
// a repo-command step's <step>-failed); a stable streak means retrying
|
|
3201
|
+
// is not going to help and parking is the right cost behavior. A
|
|
3202
|
+
// resident-step failure never parks: item 67's ladder is its escalation.
|
|
3106
3203
|
// Any other state resets the streak (below).
|
|
3107
3204
|
const prev = await this.ctx.storage.get<{ reason: string; count: number }>(DEGRADED_STREAK_KEY);
|
|
3108
3205
|
const streak =
|
|
@@ -3317,6 +3414,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3317
3414
|
// "idle since" and every attach would take the wake-fetch path).
|
|
3318
3415
|
delete updatedFacts.idleSince;
|
|
3319
3416
|
await this.ctx.storage.put(FACTS_KEY, updatedFacts);
|
|
3417
|
+
// A completed cycle ends any resident-step streak (item 67).
|
|
3418
|
+
await this.ctx.storage.delete(INFRA_STREAK_KEY);
|
|
3320
3419
|
await this.setResidentState("warm");
|
|
3321
3420
|
// Event-triggered reclamation: the prune above already told the
|
|
3322
3421
|
// mirror which branches died; finished refs give their worktree and
|
|
@@ -3370,8 +3469,57 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3370
3469
|
private async refreshFailed(failure: RefreshFailure, selfInFlight: number): Promise<void> {
|
|
3371
3470
|
console.log(`refresh: cycle failed — ${failure.reason.slice(0, 400)}`);
|
|
3372
3471
|
await this.recordRefreshError(failure.reason);
|
|
3373
|
-
|
|
3374
|
-
|
|
3472
|
+
if (failure.diskFull) {
|
|
3473
|
+
await this.setResidentState("degraded", failure.reason);
|
|
3474
|
+
await this.recoverFromDiskFull(failure.reason, selfInFlight);
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
// Item 67: a failure in the repository's own command is evidence about the
|
|
3478
|
+
// repo — degraded, and the gate's park streak decides; it also ends any
|
|
3479
|
+
// resident-step streak, since our machinery got that far. A failure in
|
|
3480
|
+
// the resident's own steps climbs the ladder over the persisted count.
|
|
3481
|
+
if (isRepoCommandStep(failure.step)) {
|
|
3482
|
+
await this.ctx.storage.delete(INFRA_STREAK_KEY);
|
|
3483
|
+
await this.setResidentState("degraded", failure.reason);
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
const row = await this.noteInfraStreak(failure.step);
|
|
3487
|
+
switch (infraStreakRung(row.count)) {
|
|
3488
|
+
case "count":
|
|
3489
|
+
await this.setResidentState("degraded", failure.reason);
|
|
3490
|
+
return;
|
|
3491
|
+
case "recreate":
|
|
3492
|
+
// The disk, not the repo: destroy the container, snapshots kept; the
|
|
3493
|
+
// next cycle's wake restores mirror, checkout and deps from R2.
|
|
3494
|
+
await this.recreateContainer(infraStreakReason(row, "recreate"));
|
|
3495
|
+
return;
|
|
3496
|
+
case "down": {
|
|
3497
|
+
// A recreated container failed the same way: the snapshot or the
|
|
3498
|
+
// resident is what is wrong, and a rebuild from GitHub is the exit —
|
|
3499
|
+
// item 36's transition, the VM destroyed first so provisioning starts
|
|
3500
|
+
// on a fresh one.
|
|
3501
|
+
const reason = infraStreakReason(row, "down");
|
|
3502
|
+
this.swapIncarnation(); // deliberate incarnation swap
|
|
3503
|
+
await this.forgetRuntimeIdentity();
|
|
3504
|
+
await this.destroy().catch((err) => console.log(`infra-streak: destroy failed: ${errMsg(err)}`));
|
|
3505
|
+
await this.ctx.storage.delete(INFRA_STREAK_KEY);
|
|
3506
|
+
await this.goDown(reason);
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
/** One more cycle failed in the resident's own steps: the count up by one,
|
|
3513
|
+
* `firstAt` kept, the step and `lastAt` now. */
|
|
3514
|
+
private async noteInfraStreak(step: string): Promise<InfraStreakRow> {
|
|
3515
|
+
const prev = await this.ctx.storage.get<InfraStreakRow>(INFRA_STREAK_KEY);
|
|
3516
|
+
const now = new Date(systemClock()).toISOString();
|
|
3517
|
+
const row: InfraStreakRow = { step, count: (prev?.count ?? 0) + 1, firstAt: prev?.firstAt ?? now, lastAt: now };
|
|
3518
|
+
await this.ctx.storage.put(INFRA_STREAK_KEY, row);
|
|
3519
|
+
console.log(
|
|
3520
|
+
`infra-streak: cycle ${row.count} of ${INFRA_STREAK_DOWN_AT} failed in the resident's own step ${step} (first at ${row.firstAt})`,
|
|
3521
|
+
);
|
|
3522
|
+
return row;
|
|
3375
3523
|
}
|
|
3376
3524
|
|
|
3377
3525
|
// -- the runtime that never answers (docs/reference/specs/resident-repos.md item 64) ------
|
|
@@ -3494,7 +3642,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3494
3642
|
* restore, for the reason `escalateRuntimeUnreachable` gives. */
|
|
3495
3643
|
private async recreateContainer(reason: string): Promise<void> {
|
|
3496
3644
|
console.log(
|
|
3497
|
-
`
|
|
3645
|
+
`recreate: destroying the container — snapshots kept; the next exec restores from R2 (${reason.slice(0, 200)})`,
|
|
3498
3646
|
);
|
|
3499
3647
|
this.swapIncarnation(); // deliberate incarnation swap
|
|
3500
3648
|
await this.forgetRuntimeIdentity();
|
|
@@ -4687,7 +4835,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4687
4835
|
// container under it (and isIdle never parks the cycle mid-attach).
|
|
4688
4836
|
this.attachesInFlight++;
|
|
4689
4837
|
try {
|
|
4690
|
-
|
|
4838
|
+
const res = await this.attachThreadBody(
|
|
4691
4839
|
threadKey,
|
|
4692
4840
|
refHint,
|
|
4693
4841
|
readonly,
|
|
@@ -4698,6 +4846,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4698
4846
|
record,
|
|
4699
4847
|
reason,
|
|
4700
4848
|
);
|
|
4849
|
+
// The run this attach opens is now in flight until its release —
|
|
4850
|
+
// whatever its op counters read between the bot's calls (item 44).
|
|
4851
|
+
if (!("error" in res)) await this.registerRun(threadKey);
|
|
4852
|
+
return res;
|
|
4701
4853
|
} finally {
|
|
4702
4854
|
this.attachesInFlight--;
|
|
4703
4855
|
}
|
|
@@ -5135,10 +5287,16 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5135
5287
|
// second movement is decided on. (Under the mirror mutex — the mirror
|
|
5136
5288
|
// cannot change in between.)
|
|
5137
5289
|
const refExists = why === null || (await this.refExists(binding.ref));
|
|
5290
|
+
// The tip is read here, once, and is what the target is judged on and
|
|
5291
|
+
// what the worktree is provisioned at: a caller that named a commit
|
|
5292
|
+
// gets that commit or a `stale-tip` refusal, never whatever the tip
|
|
5293
|
+
// happens to be (item 51).
|
|
5294
|
+
let tipSha = refExists ? await this.readMirrorSha(binding.ref).catch(() => null) : null;
|
|
5138
5295
|
let target = attachTarget({
|
|
5139
5296
|
refExists,
|
|
5140
5297
|
wantSha: want,
|
|
5141
5298
|
commitInMirror: !refExists && want !== null && (await this.commitInMirror(want)),
|
|
5299
|
+
tipSha,
|
|
5142
5300
|
});
|
|
5143
5301
|
if (target.kind === "unknown-ref" && returnable) {
|
|
5144
5302
|
// The thread's branch — one a rebind moved it onto, or one its own
|
|
@@ -5151,10 +5309,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5151
5309
|
binding = back.binding;
|
|
5152
5310
|
returned = back.returned;
|
|
5153
5311
|
want = wantShaForBinding({ boundRef: binding.ref, refHint, wantSha });
|
|
5312
|
+
const defaultExists = await this.refExists(binding.ref);
|
|
5313
|
+
tipSha = defaultExists ? await this.readMirrorSha(binding.ref).catch(() => null) : null;
|
|
5154
5314
|
target = attachTarget({
|
|
5155
|
-
refExists:
|
|
5315
|
+
refExists: defaultExists,
|
|
5156
5316
|
wantSha: want,
|
|
5157
5317
|
commitInMirror: false,
|
|
5318
|
+
tipSha,
|
|
5158
5319
|
});
|
|
5159
5320
|
}
|
|
5160
5321
|
if (target.kind === "unknown-ref") {
|
|
@@ -5164,7 +5325,19 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5164
5325
|
(want !== null ? `, and the mirror does not hold the expected commit ${want.slice(0, 7)} either` : ""),
|
|
5165
5326
|
);
|
|
5166
5327
|
}
|
|
5167
|
-
|
|
5328
|
+
if (target.kind === "stale-tip") {
|
|
5329
|
+
throw new StepError(
|
|
5330
|
+
"stale-tip",
|
|
5331
|
+
`ref ${JSON.stringify(binding.ref)} is at ${target.tip ? target.tip.slice(0, 7) : "an unreadable tip"} in the mirror${why === null ? " (no fetch was due)" : " even after the fetch"}; the caller expects ${target.want.slice(0, 7)} — the run executes at the commit it asked for, or not on this resident`,
|
|
5332
|
+
);
|
|
5333
|
+
}
|
|
5334
|
+
// A tip the read could not answer is never provisioned at: with no
|
|
5335
|
+
// commit named the target is `ref` without looking at the tip, so the
|
|
5336
|
+
// failed read is named here instead of becoming a null sha.
|
|
5337
|
+
const sha = target.kind === "sha" ? target.sha : tipSha;
|
|
5338
|
+
if (sha === null) {
|
|
5339
|
+
throw new StepError("rev-parse", `the mirror's tip of ${JSON.stringify(binding.ref)} could not be read`);
|
|
5340
|
+
}
|
|
5168
5341
|
const threadLockKey = await this.lockfileKey(sha);
|
|
5169
5342
|
const recreated = await this.ensureThreadWorktree(binding, sha, mode.originUrl, mode.modeSwitch, {
|
|
5170
5343
|
detached: target.kind === "sha",
|
|
@@ -5186,6 +5359,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5186
5359
|
if (err instanceof StepError && err.step === "unknown-ref") {
|
|
5187
5360
|
return { error: `unknown-ref: ${err.message}`, status: 400 };
|
|
5188
5361
|
}
|
|
5362
|
+
if (err instanceof StepError && err.step === "stale-tip") {
|
|
5363
|
+
// Item 51: not a resident fault and not a caller fault — a fact about
|
|
5364
|
+
// the mirror at this instant. 409 with the state, so the bot's named
|
|
5365
|
+
// fallback runs cold at the commit it asked for.
|
|
5366
|
+
const s = await this.getStatus();
|
|
5367
|
+
return { error: `stale-tip: ${err.message}`, status: 409, state: s.state, reason: "stale-tip" };
|
|
5368
|
+
}
|
|
5189
5369
|
return this.attachFailed(err);
|
|
5190
5370
|
}
|
|
5191
5371
|
|
|
@@ -6127,11 +6307,30 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6127
6307
|
return res;
|
|
6128
6308
|
}
|
|
6129
6309
|
|
|
6310
|
+
/** The exec route's one gate for a runtime replacement (item 43): every
|
|
6311
|
+
* `RuntimeReplacedError` the route meets — the preflight's own probe of the
|
|
6312
|
+
* worktree, the command itself — is judged by `replacedExecAnswer`, so the
|
|
6313
|
+
* word reaches the harness only when the SDK vouched the runtime moved or
|
|
6314
|
+
* the resident knows the container it held is gone. */
|
|
6130
6315
|
private async execThreadImpl(
|
|
6131
6316
|
threadKey: string,
|
|
6132
6317
|
command: string,
|
|
6133
6318
|
timeoutMs: number,
|
|
6134
6319
|
env?: Record<string, string>,
|
|
6320
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number; truncated: boolean } | ThreadErr> {
|
|
6321
|
+
try {
|
|
6322
|
+
return await this.execThreadBody(threadKey, command, timeoutMs, env);
|
|
6323
|
+
} catch (err) {
|
|
6324
|
+
if (err instanceof RuntimeReplacedError) return this.replacedExecAnswer(err);
|
|
6325
|
+
throw err;
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
|
|
6329
|
+
private async execThreadBody(
|
|
6330
|
+
threadKey: string,
|
|
6331
|
+
command: string,
|
|
6332
|
+
timeoutMs: number,
|
|
6333
|
+
env?: Record<string, string>,
|
|
6135
6334
|
): Promise<{ stdout: string; stderr: string; exitCode: number; truncated: boolean } | ThreadErr> {
|
|
6136
6335
|
const pre = await this.threadPreflight(threadKey);
|
|
6137
6336
|
if ("error" in pre) return pre;
|
|
@@ -6147,13 +6346,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6147
6346
|
: {}),
|
|
6148
6347
|
} satisfies ThreadBinding);
|
|
6149
6348
|
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
r = await this.threadRunCapped(binding.user, binding.worktreePath, command, timeoutMs, EXEC_OUTPUT_CAP, env);
|
|
6153
|
-
} catch (err) {
|
|
6154
|
-
if (err instanceof RuntimeReplacedError) return runtimeReplacedErr(err);
|
|
6155
|
-
throw err;
|
|
6156
|
-
}
|
|
6349
|
+
// A runtime replacement under the command propagates to `execThreadImpl`'s gate.
|
|
6350
|
+
const r = await this.threadRunCapped(binding.user, binding.worktreePath, command, timeoutMs, EXEC_OUTPUT_CAP, env);
|
|
6157
6351
|
const truncated = r.stdout.length > EXEC_OUTPUT_CAP || r.stderr.length > EXEC_OUTPUT_CAP || r.truncated === true;
|
|
6158
6352
|
const notes: string[] = [];
|
|
6159
6353
|
if (r.timedOut)
|
|
@@ -6361,6 +6555,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6361
6555
|
evictedLeftBehind: tree && "leftBehind" in tree ? tree.leftBehind : undefined,
|
|
6362
6556
|
evictedUnmeasured: tree && "unmeasured" in tree ? tree.unmeasured : undefined,
|
|
6363
6557
|
} satisfies ThreadBinding);
|
|
6558
|
+
// The run's registration goes with its binding (item 44): released here,
|
|
6559
|
+
// never on a give-way above — a re-attach has re-registered it anyway.
|
|
6560
|
+
await this.ctx.storage.delete(runRegKey(binding.threadKey));
|
|
6364
6561
|
if (tree) console.log(`${logCtx}: ${binding.threadKey} evicted (${why}) — ${evictedTreeSentence(tree)}`);
|
|
6365
6562
|
return true;
|
|
6366
6563
|
}
|
|
@@ -6522,10 +6719,33 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6522
6719
|
return threadOps + this.opUsersInUse.size + this.attachesInFlight;
|
|
6523
6720
|
}
|
|
6524
6721
|
/** In-flight activity for the deploy preflight (GET /status, GET /residents).
|
|
6525
|
-
*
|
|
6526
|
-
* survived to be interrupted
|
|
6722
|
+
* The in-memory counters (a fresh isolate answers 0 for them — nothing of
|
|
6723
|
+
* THEIRS survived to be interrupted) plus the durable run registrations:
|
|
6724
|
+
* a harness run's process DOES survive an isolate swap in the container,
|
|
6725
|
+
* so its registration must too. */
|
|
6527
6726
|
async getInFlightCount(): Promise<number> {
|
|
6528
|
-
return this.inFlightCount();
|
|
6727
|
+
return this.inFlightCount() + (await this.registeredRunsBeyondOps());
|
|
6728
|
+
}
|
|
6729
|
+
/** A run holds its worktree from `/attach` to its release (`/detach`, the
|
|
6730
|
+
* sweep, disk pressure — every path ends in `evictBinding`, which clears
|
|
6731
|
+
* the row). Written by the attach; idempotent, a re-attach refreshes it. */
|
|
6732
|
+
private async registerRun(threadKey: string): Promise<void> {
|
|
6733
|
+
await this.ctx.storage.put(runRegKey(threadKey), {
|
|
6734
|
+
threadKey,
|
|
6735
|
+
registeredAt: new Date(systemClock()).toISOString(),
|
|
6736
|
+
} satisfies RunRegistration);
|
|
6737
|
+
}
|
|
6738
|
+
/** The registrations the op counters do not already see: a thread with an
|
|
6739
|
+
* op in flight is counted by `runsInFlightCount`, so its registration is
|
|
6740
|
+
* not counted again. Deliberately NOT part of `inFlightCount()`: the
|
|
6741
|
+
* container-lifecycle predicates (isIdle, reconcileImage) must not let a
|
|
6742
|
+
* stale registration pin a container awake — the clean-idle sweep is what
|
|
6743
|
+
* drains a registration whose release never came. */
|
|
6744
|
+
private async registeredRunsBeyondOps(): Promise<number> {
|
|
6745
|
+
const regs = await this.ctx.storage.list<RunRegistration>({ prefix: RUN_REG_KEY_PREFIX });
|
|
6746
|
+
let n = 0;
|
|
6747
|
+
for (const r of regs.values()) if ((this.threadOpsInFlight.get(r.threadKey) ?? 0) === 0) n++;
|
|
6748
|
+
return n;
|
|
6529
6749
|
}
|
|
6530
6750
|
private attachesInFlight = 0;
|
|
6531
6751
|
/** A refresh cycle past its idle/reconcile gates (fetching, rebuilding, snapshotting). */
|
|
@@ -7068,6 +7288,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7068
7288
|
REFRESH_INSTANCE_KEY,
|
|
7069
7289
|
RUNTIME_UNREACHABLE_KEY,
|
|
7070
7290
|
AUTO_REBUILDS_KEY,
|
|
7291
|
+
INFRA_STREAK_KEY,
|
|
7071
7292
|
]);
|
|
7072
7293
|
const facts = map.get(FACTS_KEY) as RepoFacts | undefined;
|
|
7073
7294
|
const snap = map.get(SNAPSHOT_KEY) as SnapshotRecord | undefined;
|
|
@@ -7120,6 +7341,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7120
7341
|
recycledUnmeasured: recycledUnmeasured ?? null,
|
|
7121
7342
|
}),
|
|
7122
7343
|
);
|
|
7344
|
+
// The runs whose process lives in the container between operator calls
|
|
7345
|
+
// (item 44): what the deploy preflight must see, added to both counts.
|
|
7346
|
+
const registeredRuns = await this.registeredRunsBeyondOps();
|
|
7123
7347
|
return {
|
|
7124
7348
|
resource: map.get(RESOURCE_KEY) ?? null,
|
|
7125
7349
|
state: map.get(STATE_KEY) ?? "down",
|
|
@@ -7139,9 +7363,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7139
7363
|
provisionRun: provisionRun.length,
|
|
7140
7364
|
provisionDeadline: provisionDeadline.length,
|
|
7141
7365
|
},
|
|
7142
|
-
inFlight: this.inFlightCount(),
|
|
7366
|
+
inFlight: this.inFlightCount() + registeredRuns,
|
|
7143
7367
|
// The runs alone (no refresh cycle): what the deploy preflight refuses on.
|
|
7144
|
-
runsInFlight: this.runsInFlightCount(),
|
|
7368
|
+
runsInFlight: this.runsInFlightCount() + registeredRuns,
|
|
7145
7369
|
// Item 22: who holds what, as the rows say — the mirror mutex and the
|
|
7146
7370
|
// cycle/hydration leases, each judged against this incarnation.
|
|
7147
7371
|
incarnation: this.incarnation,
|
|
@@ -7173,6 +7397,12 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7173
7397
|
// Item 36: the auto-rebuilds this resident has had (ISO instants; the
|
|
7174
7398
|
// budget's window is judged over them). Empty after a person's rebuild.
|
|
7175
7399
|
autoRebuilds: (map.get(AUTO_REBUILDS_KEY) as string[] | undefined) ?? [],
|
|
7400
|
+
// Item 67: consecutive cycles failed in the resident's own steps, with
|
|
7401
|
+
// the rung that count is on; null while cycles complete.
|
|
7402
|
+
infraStreak: (() => {
|
|
7403
|
+
const row = map.get(INFRA_STREAK_KEY) as InfraStreakRow | undefined;
|
|
7404
|
+
return row ? { ...row, rung: infraStreakRung(row.count) } : null;
|
|
7405
|
+
})(),
|
|
7176
7406
|
};
|
|
7177
7407
|
}
|
|
7178
7408
|
|
|
@@ -7255,6 +7485,37 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7255
7485
|
return { recreated: true, restoreStartedAt };
|
|
7256
7486
|
}
|
|
7257
7487
|
|
|
7488
|
+
/** Fault injection for item 67: write the resident-step streak row at `count`
|
|
7489
|
+
* (0 deletes it), so the next real failure lands on the rung under test.
|
|
7490
|
+
* Test-only semantics; admin scope. */
|
|
7491
|
+
async debugSetInfraStreak(count: number): Promise<{ infraStreak: (InfraStreakRow & { rung: string }) | null }> {
|
|
7492
|
+
if (count === 0) {
|
|
7493
|
+
await this.ctx.storage.delete(INFRA_STREAK_KEY);
|
|
7494
|
+
return { infraStreak: null };
|
|
7495
|
+
}
|
|
7496
|
+
const now = new Date(systemClock()).toISOString();
|
|
7497
|
+
const row: InfraStreakRow = { step: "injected", count, firstAt: now, lastAt: now };
|
|
7498
|
+
await this.ctx.storage.put(INFRA_STREAK_KEY, row);
|
|
7499
|
+
return { infraStreak: { ...row, rung: infraStreakRung(count) } };
|
|
7500
|
+
}
|
|
7501
|
+
|
|
7502
|
+
/** Fault injection for item 67: corrupt the mirror on disk so the next
|
|
7503
|
+
* cycle's `fetch` fails in a resident step. The mirror's `config` goes (its
|
|
7504
|
+
* `origin` remote with it: `'origin' does not appear to be a git
|
|
7505
|
+
* repository`, exit 128) — NOT its objects: the wake check (`readyStamp`)
|
|
7506
|
+
* tests `objects/`, and a missing directory is a disk to restore, which the
|
|
7507
|
+
* wake did before any step could fail the first time this was tried live.
|
|
7508
|
+
* The snapshots in R2 are untouched — the ladder's recreate restores them.
|
|
7509
|
+
* Test-only semantics; admin scope. */
|
|
7510
|
+
async debugBreakMirror(): Promise<{ broken: boolean; error?: string }> {
|
|
7511
|
+
try {
|
|
7512
|
+
await this.runOk(["rm", "-f", `${MIRROR_DIR}/config`], "break-mirror");
|
|
7513
|
+
return { broken: true };
|
|
7514
|
+
} catch (err) {
|
|
7515
|
+
return { broken: false, error: errMsg(err) };
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
|
|
7258
7519
|
/** Fault injection for item 36 without corrupting real R2 objects. Bare: persist
|
|
7259
7520
|
* `down` with a rehydration-flavored reason and nothing else, so the next
|
|
7260
7521
|
* watchdog pass is the backstop under test. `transition`: go through
|
|
@@ -8644,6 +8905,19 @@ async function handleDebug(env: Env, body: Record<string, unknown>): Promise<Res
|
|
|
8644
8905
|
}
|
|
8645
8906
|
case "force-onboarding":
|
|
8646
8907
|
return json(await stub.debugForceOnboarding());
|
|
8908
|
+
case "infra-streak": {
|
|
8909
|
+
// Fault injection for item 67: set the resident-step streak's count, so
|
|
8910
|
+
// one real failure lands on the rung under test (recreate at 3, down at 5).
|
|
8911
|
+
const count =
|
|
8912
|
+
typeof body.count === "number" && Number.isInteger(body.count) && body.count >= 0 ? body.count : null;
|
|
8913
|
+
if (count === null) return json({ error: "count must be a non-negative integer" }, 400);
|
|
8914
|
+
return json(await stub.debugSetInfraStreak(count));
|
|
8915
|
+
}
|
|
8916
|
+
case "break-mirror":
|
|
8917
|
+
// Fault injection for item 67: remove the mirror's objects, so the next
|
|
8918
|
+
// cycle's fetch fails in a resident step — a corrupt disk, which the
|
|
8919
|
+
// ladder's recreate restores from the snapshot. Test residents only.
|
|
8920
|
+
return json(await stub.debugBreakMirror());
|
|
8647
8921
|
case "force-down": {
|
|
8648
8922
|
// Fault injection for item 36; a rehydration-flavored default reason
|
|
8649
8923
|
// makes it eligible. `transition: true` fires the down through goDown
|
|
@@ -87,8 +87,10 @@ import {
|
|
|
87
87
|
SEED_ABANDONED_RESTORE_WAIT_MS,
|
|
88
88
|
seedFixupScript,
|
|
89
89
|
seedMarkerText,
|
|
90
|
+
type RestorePhases,
|
|
90
91
|
type SandboxSeed,
|
|
91
92
|
type SeedAnswer,
|
|
93
|
+
type SeedPhases,
|
|
92
94
|
type SeedStep,
|
|
93
95
|
} from "../../src/execution/seedPlan.js";
|
|
94
96
|
import { shellQuote } from "../../src/execution/shellQuote.js";
|
|
@@ -450,12 +452,13 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
450
452
|
// marker for another handle.
|
|
451
453
|
await this.runRoot(["sh", "-c", `${unmountAllRestoresScript()}\n${this.seedSweep()}`], 60_000);
|
|
452
454
|
let t = systemClock();
|
|
453
|
-
await this.restoreSeedInto(seed.checkoutBackupId, SEED_CHECKOUT_DIR, deadline, "checkout");
|
|
455
|
+
const checkout = await this.restoreSeedInto(seed.checkoutBackupId, SEED_CHECKOUT_DIR, deadline, "checkout");
|
|
454
456
|
steps.restore = systemClock() - t;
|
|
457
|
+
const phases: SeedPhases = { checkout, deps: null };
|
|
455
458
|
if (seed.depsBackupId) {
|
|
456
459
|
step = "deps";
|
|
457
460
|
t = systemClock();
|
|
458
|
-
await this.restoreSeedInto(seed.depsBackupId, SEED_DEPS_STAGING_DIR, deadline, "deps");
|
|
461
|
+
phases.deps = await this.restoreSeedInto(seed.depsBackupId, SEED_DEPS_STAGING_DIR, deadline, "deps");
|
|
459
462
|
steps.deps = systemClock() - t;
|
|
460
463
|
}
|
|
461
464
|
step = "fixup";
|
|
@@ -480,9 +483,27 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
480
483
|
);
|
|
481
484
|
const ms = systemClock() - t0;
|
|
482
485
|
console.log(
|
|
483
|
-
JSON.stringify({
|
|
486
|
+
JSON.stringify({
|
|
487
|
+
event: "sandbox.seeded",
|
|
488
|
+
slug: seed.slug,
|
|
489
|
+
ref: seed.fetchRef ?? seed.ref,
|
|
490
|
+
sha,
|
|
491
|
+
steps,
|
|
492
|
+
phases,
|
|
493
|
+
ms,
|
|
494
|
+
}),
|
|
484
495
|
);
|
|
485
|
-
return {
|
|
496
|
+
return {
|
|
497
|
+
seeded: true,
|
|
498
|
+
cached: false,
|
|
499
|
+
slug: seed.slug,
|
|
500
|
+
ref: seed.fetchRef ?? seed.ref,
|
|
501
|
+
sha,
|
|
502
|
+
from,
|
|
503
|
+
steps,
|
|
504
|
+
phases,
|
|
505
|
+
ms,
|
|
506
|
+
};
|
|
486
507
|
} catch (err) {
|
|
487
508
|
const shape = thrownShape(err);
|
|
488
509
|
// A half seed never survives either: the run that follows goes cold
|
|
@@ -510,7 +531,12 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
510
531
|
* a staging sibling, the wait is judged by bytes arriving (the SDK's call
|
|
511
532
|
* takes no timeout) against the seed's one deadline, then the extract
|
|
512
533
|
* script puts a real tree in place and the target appears last. */
|
|
513
|
-
private async restoreSeedInto(
|
|
534
|
+
private async restoreSeedInto(
|
|
535
|
+
id: string,
|
|
536
|
+
targetDir: string,
|
|
537
|
+
deadlineMs: number,
|
|
538
|
+
what: string,
|
|
539
|
+
): Promise<RestorePhases> {
|
|
514
540
|
const attempt = crypto.randomUUID().slice(0, 8);
|
|
515
541
|
const mountDir = restoreMountDir(targetDir, attempt);
|
|
516
542
|
const backup: DirectoryBackup = { id, dir: mountDir };
|
|
@@ -535,11 +561,18 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
535
561
|
const verdict = judgeRestoreProgress({ startedMs, nowMs: systemClock(), samples, deadlineMs });
|
|
536
562
|
if (verdict.verdict !== "wait") throw new Error(`${what} restore ${verdict.verdict}: ${verdict.detail}`);
|
|
537
563
|
}
|
|
564
|
+
const download = systemClock() - startedMs;
|
|
565
|
+
const extractStartedMs = systemClock();
|
|
538
566
|
const r = await this.runRoot(
|
|
539
567
|
["sh", "-c", extractRestoreScript({ mountDir, backupId: id, archivePath: restoreArchivePath(id), targetDir })],
|
|
540
568
|
Math.max(60_000, deadlineMs - systemClock()),
|
|
541
569
|
);
|
|
542
570
|
if (r.exitCode !== 0) throw new Error(`${what} extract exited ${r.exitCode}: ${tail(r.stderr || r.stdout)}`);
|
|
571
|
+
const extract = systemClock() - extractStartedMs;
|
|
572
|
+
console.log(
|
|
573
|
+
JSON.stringify({ event: "sandbox.seed-restore", what, id, download, extract, method: r.stdout.trim() }),
|
|
574
|
+
);
|
|
575
|
+
return { download, extract };
|
|
543
576
|
}
|
|
544
577
|
|
|
545
578
|
/** Restores this object started that have not settled: what a failure
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.243.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.243.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -20445,7 +20445,7 @@
|
|
|
20445
20445
|
},
|
|
20446
20446
|
"packages/switchboard": {
|
|
20447
20447
|
"name": "@coreplane/switchboard",
|
|
20448
|
-
"version": "1.
|
|
20448
|
+
"version": "1.243.0",
|
|
20449
20449
|
"license": "Apache-2.0",
|
|
20450
20450
|
"dependencies": {
|
|
20451
20451
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.243.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|