@coreplane/switchboard 1.241.0 → 1.242.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 +25 -12
- package/dist/assets/deploy/cloudflare-resident/worker.ts +197 -15
- 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 +2 -2
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +31 -14
- 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/types.ts +12 -0
- package/dist/assets/src/core/coordinator/contract.ts +3 -1
- package/dist/assets/src/core/costs.ts +12 -4
- package/dist/assets/src/core/harness/scope.ts +21 -0
- package/dist/assets/src/core/runEvents.ts +20 -1
- package/dist/assets/src/core/runLedger/types.ts +2 -0
- package/dist/assets/src/core/runRecord.ts +4 -0
- package/dist/assets/src/core/ship/contract.ts +4 -2
- 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 +15 -1
- 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 +419 -425
- package/dist/assets/web/dist/assets/{AppShell-DEy5jRuS.js → AppShell-IDMGG6yi.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-BuHD-Bv7.js → CostsPage-CtmKhhOF.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-BGpPr5xr.js → DeliveryPage-n1tz5I_v.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DtmQM2gD.js → NotFoundPage-C97EhJcQ.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-CrEAWx5b.js → PendingTurnRow-BZA_vQt3.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-C-48fxat.js → ResidentDetailPage-DTMBgnIW.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-CBBIjPOa.js → ResidentsIndexPage-CaDvXhzJ.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-CtNGNnhN.js → RunsTabs-BUfdk0lH.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-Ci1Ygqnc.js → ScheduledPage-DXD2gLJk.js} +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-DPWE1JmO.js → StatusDot-ELoXHlFt.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DKhSRH9t.js → Tooltip-BoeFwYP2.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.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-BU5UivXC.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/{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/{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-B2fX10aW.css +1 -0
- package/dist/assets/web/dist/assets/{main-B3is4sk6.js → main-D4EA1g6n.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-g7ml86LM.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 +1390 -484
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +0 -2
- 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/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/main-DP_zSemY.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-yji8a1wM.js +0 -9
|
@@ -100,10 +100,16 @@ defaults:
|
|
|
100
100
|
# maxMinutes: 45
|
|
101
101
|
# maxIdentity: read
|
|
102
102
|
|
|
103
|
-
# Static per-user defaults (user ID -> scope).
|
|
103
|
+
# Static per-user defaults (user ID -> scope). A user's `harness` puts that
|
|
104
|
+
# person's runs of a preset on another harness (docs/reference/specs/harness.md
|
|
105
|
+
# item 8) — every run of it they request, in any channel, and nobody else's; the
|
|
106
|
+
# same words as the top-level `harness` block, which they override (a channel's
|
|
107
|
+
# `harness` sits between). From chat: `config set me --harness.coding opencode`.
|
|
104
108
|
# users:
|
|
105
109
|
# slack:U012345:
|
|
106
110
|
# model: openai/gpt-5
|
|
111
|
+
# harness:
|
|
112
|
+
# coding: opencode
|
|
107
113
|
|
|
108
114
|
# Authorization (docs/reference/specs/authorization.md item 9, docs/reference/authorization.md):
|
|
109
115
|
# two blocks. `restrict` names what is CLOSED unless granted — an agent listed
|
|
@@ -327,17 +333,22 @@ workspaceDir: ./workspaces
|
|
|
327
333
|
# # a finding at or above it sends the round into a fix step instead of merge-ready.
|
|
328
334
|
# # Per channel/user: config set … --ship.addressSeverity; per run: severity:<level>.
|
|
329
335
|
|
|
330
|
-
# Which harness each preset's runs are driven by
|
|
331
|
-
# item 8): a mapping of preset to a harness's
|
|
332
|
-
# coding agent, the default for every preset)
|
|
333
|
-
# `opencode serve`) — each started inside
|
|
334
|
-
# as a child of the bot for a preset
|
|
335
|
-
# model-proxy bearer as its only key, every
|
|
336
|
-
# event on the run's record.
|
|
337
|
-
#
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
#
|
|
336
|
+
# Which harness each preset's runs are driven by, deployment-wide
|
|
337
|
+
# (docs/reference/specs/harness.md item 8): a mapping of preset to a harness's
|
|
338
|
+
# name. Two exist — `pi` (the pi coding agent, the default for every preset)
|
|
339
|
+
# and `opencode` (OpenCode's server, `opencode serve`) — each started inside
|
|
340
|
+
# the run's own execution container (or as a child of the bot for a preset
|
|
341
|
+
# without a workspace) with the run's model-proxy bearer as its only key, every
|
|
342
|
+
# tool call decided in the bot, every event on the run's record. This block is
|
|
343
|
+
# the defaults layer of the per-scope `harness` setting: a channel's or a
|
|
344
|
+
# user's `harness` (above, or `config set channel|me --harness.<preset> …`)
|
|
345
|
+
# overrides it for the runs in that scope, user over channel over here. A
|
|
346
|
+
# preset no layer names runs on pi; nothing defaults to OpenCode. A run keeps
|
|
347
|
+
# the harness it started on, so changing a word moves the next run, never one
|
|
348
|
+
# in flight. Any other word (`native`, the deleted loop; `codex`) fails the
|
|
349
|
+
# load naming the two, as does an unknown preset, in every layer; a
|
|
350
|
+
# `defaults.harness` key is refused pointing here. How-to:
|
|
351
|
+
# docs/how-to/put-a-preset-on-opencode.md.
|
|
341
352
|
# harness:
|
|
342
353
|
# coding: opencode # coding runs on OpenCode
|
|
343
354
|
# review: pi # the default, written out
|
|
@@ -411,6 +422,8 @@ workspaceDir: ./workspaces
|
|
|
411
422
|
# snapshot: # the page serves a snapshot of both billing sources, never a live read;
|
|
412
423
|
# everyHours: 24 # how often the bot reads them (1–168, default 24). `costs snapshot`
|
|
413
424
|
# # takes one now. Kept on the state Worker any `*.worker` block names
|
|
425
|
+
# alertChannel: slack:C0123ABCD # optional: a line there after three failed takes in a row, and one
|
|
426
|
+
# # when a take lands again; otherwise only the page's status line says
|
|
414
427
|
# groups:
|
|
415
428
|
# myapp:
|
|
416
429
|
# label: My App
|
|
@@ -197,6 +197,14 @@ import {
|
|
|
197
197
|
type RefreshFailure,
|
|
198
198
|
} from "../../src/execution/residentRefresh.js";
|
|
199
199
|
import { autoRebuildDecision, isAutoRebuildEligible } from "../../src/execution/residentAutoRebuild.js";
|
|
200
|
+
import {
|
|
201
|
+
INFRA_STREAK_DOWN_AT,
|
|
202
|
+
infraStreakReason,
|
|
203
|
+
infraStreakRung,
|
|
204
|
+
type InfraStreakRow,
|
|
205
|
+
isRepoCommandStep,
|
|
206
|
+
parksOnRepeat,
|
|
207
|
+
} from "../../src/execution/residentInfraStreak.js";
|
|
200
208
|
import {
|
|
201
209
|
lifecycleOf,
|
|
202
210
|
parseLifecycle,
|
|
@@ -544,6 +552,8 @@ const GITHUB_API_TIMEOUT_MS = 10_000;
|
|
|
544
552
|
* idle-park like a warm one; the next attach still refreshes first. */
|
|
545
553
|
const DEGRADED_PARK_AFTER_CYCLES = 3;
|
|
546
554
|
const DEGRADED_STREAK_KEY = "resident:degradedStreak";
|
|
555
|
+
/** Consecutive cycles failed in the resident's OWN steps (item 67): the ladder's count, `InfraStreakRow`. */
|
|
556
|
+
const INFRA_STREAK_KEY = "resident:infraStreak";
|
|
547
557
|
/** Degraded reasons that are not evidence about the repository — both always
|
|
548
558
|
* carry a `: detail` suffix: the watchdog's `stale-mid-flight: …` (a marker
|
|
549
559
|
* a dead cycle left behind; a cycle must run) and the wake path's
|
|
@@ -561,7 +571,7 @@ const DEGRADED_STREAK_KEY = "resident:degradedStreak";
|
|
|
561
571
|
* re-enters the step. A control port that did not answer
|
|
562
572
|
* (`runtime-unreachable: …`, item 64) is the third: no command ran, the
|
|
563
573
|
* ladder over its own persisted count owns the recovery. */
|
|
564
|
-
const NON_EVIDENCE_REASON = /^(?:stale-mid-flight|restore-interrupted|runtime-unreachable):/;
|
|
574
|
+
const NON_EVIDENCE_REASON = /^(?:stale-mid-flight|restore-interrupted|runtime-unreachable|infra-streak):/;
|
|
565
575
|
/** When the disk-full recovery last stopped the container (docs/reference/specs/resident-repos.md item 54):
|
|
566
576
|
* feeds `planDiskFullRecovery`'s cooldown so a working set that refills the
|
|
567
577
|
* disk is named, not recycled in a loop. */
|
|
@@ -1479,6 +1489,17 @@ const threadBindingKey = (threadKey: string) => `${THREAD_KEY_PREFIX}${threadKey
|
|
|
1479
1489
|
|
|
1480
1490
|
const parentDir = (p: string): string => p.slice(0, p.lastIndexOf("/"));
|
|
1481
1491
|
|
|
1492
|
+
/** A run's registration, held from its attach to its release (item 44): a
|
|
1493
|
+
* harness run's process lives in the container BETWEEN the bot's operator
|
|
1494
|
+
* calls, so the in-memory op counters read 0 while it runs. Durable rows —
|
|
1495
|
+
* a fresh isolate still counts the process that survived in the container. */
|
|
1496
|
+
const RUN_REG_KEY_PREFIX = "runReg:";
|
|
1497
|
+
const runRegKey = (threadKey: string) => `${RUN_REG_KEY_PREFIX}${threadKey}`;
|
|
1498
|
+
interface RunRegistration {
|
|
1499
|
+
threadKey: string;
|
|
1500
|
+
registeredAt: string;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1482
1503
|
/** Deterministic per-thread+ref worktree path. Slugs replace anything outside
|
|
1483
1504
|
* [A-Za-z0-9._-]; a threadKey-derived hash suffix keeps two keys that slug
|
|
1484
1505
|
* identically (e.g. "a:b" vs "a-b") from colliding on disk. */
|
|
@@ -3099,10 +3120,12 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3099
3120
|
}
|
|
3100
3121
|
}
|
|
3101
3122
|
let settled = entry.state === "warm";
|
|
3102
|
-
if (entry.state === "degraded" && !isNonEvidenceReason(entry.reason)) {
|
|
3123
|
+
if (entry.state === "degraded" && !isNonEvidenceReason(entry.reason) && parksOnRepeat(entry.reason)) {
|
|
3103
3124
|
// Count consecutive cycles that found the same REFRESH-PRODUCED degraded
|
|
3104
|
-
// reason (github-unreachable,
|
|
3105
|
-
//
|
|
3125
|
+
// reason that is evidence about the repo or GitHub (github-unreachable,
|
|
3126
|
+
// a repo-command step's <step>-failed); a stable streak means retrying
|
|
3127
|
+
// is not going to help and parking is the right cost behavior. A
|
|
3128
|
+
// resident-step failure never parks: item 67's ladder is its escalation.
|
|
3106
3129
|
// Any other state resets the streak (below).
|
|
3107
3130
|
const prev = await this.ctx.storage.get<{ reason: string; count: number }>(DEGRADED_STREAK_KEY);
|
|
3108
3131
|
const streak =
|
|
@@ -3317,6 +3340,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3317
3340
|
// "idle since" and every attach would take the wake-fetch path).
|
|
3318
3341
|
delete updatedFacts.idleSince;
|
|
3319
3342
|
await this.ctx.storage.put(FACTS_KEY, updatedFacts);
|
|
3343
|
+
// A completed cycle ends any resident-step streak (item 67).
|
|
3344
|
+
await this.ctx.storage.delete(INFRA_STREAK_KEY);
|
|
3320
3345
|
await this.setResidentState("warm");
|
|
3321
3346
|
// Event-triggered reclamation: the prune above already told the
|
|
3322
3347
|
// mirror which branches died; finished refs give their worktree and
|
|
@@ -3370,8 +3395,57 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3370
3395
|
private async refreshFailed(failure: RefreshFailure, selfInFlight: number): Promise<void> {
|
|
3371
3396
|
console.log(`refresh: cycle failed — ${failure.reason.slice(0, 400)}`);
|
|
3372
3397
|
await this.recordRefreshError(failure.reason);
|
|
3373
|
-
|
|
3374
|
-
|
|
3398
|
+
if (failure.diskFull) {
|
|
3399
|
+
await this.setResidentState("degraded", failure.reason);
|
|
3400
|
+
await this.recoverFromDiskFull(failure.reason, selfInFlight);
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
// Item 67: a failure in the repository's own command is evidence about the
|
|
3404
|
+
// repo — degraded, and the gate's park streak decides; it also ends any
|
|
3405
|
+
// resident-step streak, since our machinery got that far. A failure in
|
|
3406
|
+
// the resident's own steps climbs the ladder over the persisted count.
|
|
3407
|
+
if (isRepoCommandStep(failure.step)) {
|
|
3408
|
+
await this.ctx.storage.delete(INFRA_STREAK_KEY);
|
|
3409
|
+
await this.setResidentState("degraded", failure.reason);
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
const row = await this.noteInfraStreak(failure.step);
|
|
3413
|
+
switch (infraStreakRung(row.count)) {
|
|
3414
|
+
case "count":
|
|
3415
|
+
await this.setResidentState("degraded", failure.reason);
|
|
3416
|
+
return;
|
|
3417
|
+
case "recreate":
|
|
3418
|
+
// The disk, not the repo: destroy the container, snapshots kept; the
|
|
3419
|
+
// next cycle's wake restores mirror, checkout and deps from R2.
|
|
3420
|
+
await this.recreateContainer(infraStreakReason(row, "recreate"));
|
|
3421
|
+
return;
|
|
3422
|
+
case "down": {
|
|
3423
|
+
// A recreated container failed the same way: the snapshot or the
|
|
3424
|
+
// resident is what is wrong, and a rebuild from GitHub is the exit —
|
|
3425
|
+
// item 36's transition, the VM destroyed first so provisioning starts
|
|
3426
|
+
// on a fresh one.
|
|
3427
|
+
const reason = infraStreakReason(row, "down");
|
|
3428
|
+
this.swapIncarnation(); // deliberate incarnation swap
|
|
3429
|
+
await this.forgetRuntimeIdentity();
|
|
3430
|
+
await this.destroy().catch((err) => console.log(`infra-streak: destroy failed: ${errMsg(err)}`));
|
|
3431
|
+
await this.ctx.storage.delete(INFRA_STREAK_KEY);
|
|
3432
|
+
await this.goDown(reason);
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
/** One more cycle failed in the resident's own steps: the count up by one,
|
|
3439
|
+
* `firstAt` kept, the step and `lastAt` now. */
|
|
3440
|
+
private async noteInfraStreak(step: string): Promise<InfraStreakRow> {
|
|
3441
|
+
const prev = await this.ctx.storage.get<InfraStreakRow>(INFRA_STREAK_KEY);
|
|
3442
|
+
const now = new Date(systemClock()).toISOString();
|
|
3443
|
+
const row: InfraStreakRow = { step, count: (prev?.count ?? 0) + 1, firstAt: prev?.firstAt ?? now, lastAt: now };
|
|
3444
|
+
await this.ctx.storage.put(INFRA_STREAK_KEY, row);
|
|
3445
|
+
console.log(
|
|
3446
|
+
`infra-streak: cycle ${row.count} of ${INFRA_STREAK_DOWN_AT} failed in the resident's own step ${step} (first at ${row.firstAt})`,
|
|
3447
|
+
);
|
|
3448
|
+
return row;
|
|
3375
3449
|
}
|
|
3376
3450
|
|
|
3377
3451
|
// -- the runtime that never answers (docs/reference/specs/resident-repos.md item 64) ------
|
|
@@ -3494,7 +3568,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3494
3568
|
* restore, for the reason `escalateRuntimeUnreachable` gives. */
|
|
3495
3569
|
private async recreateContainer(reason: string): Promise<void> {
|
|
3496
3570
|
console.log(
|
|
3497
|
-
`
|
|
3571
|
+
`recreate: destroying the container — snapshots kept; the next exec restores from R2 (${reason.slice(0, 200)})`,
|
|
3498
3572
|
);
|
|
3499
3573
|
this.swapIncarnation(); // deliberate incarnation swap
|
|
3500
3574
|
await this.forgetRuntimeIdentity();
|
|
@@ -4687,7 +4761,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4687
4761
|
// container under it (and isIdle never parks the cycle mid-attach).
|
|
4688
4762
|
this.attachesInFlight++;
|
|
4689
4763
|
try {
|
|
4690
|
-
|
|
4764
|
+
const res = await this.attachThreadBody(
|
|
4691
4765
|
threadKey,
|
|
4692
4766
|
refHint,
|
|
4693
4767
|
readonly,
|
|
@@ -4698,6 +4772,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4698
4772
|
record,
|
|
4699
4773
|
reason,
|
|
4700
4774
|
);
|
|
4775
|
+
// The run this attach opens is now in flight until its release —
|
|
4776
|
+
// whatever its op counters read between the bot's calls (item 44).
|
|
4777
|
+
if (!("error" in res)) await this.registerRun(threadKey);
|
|
4778
|
+
return res;
|
|
4701
4779
|
} finally {
|
|
4702
4780
|
this.attachesInFlight--;
|
|
4703
4781
|
}
|
|
@@ -5135,10 +5213,16 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5135
5213
|
// second movement is decided on. (Under the mirror mutex — the mirror
|
|
5136
5214
|
// cannot change in between.)
|
|
5137
5215
|
const refExists = why === null || (await this.refExists(binding.ref));
|
|
5216
|
+
// The tip is read here, once, and is what the target is judged on and
|
|
5217
|
+
// what the worktree is provisioned at: a caller that named a commit
|
|
5218
|
+
// gets that commit or a `stale-tip` refusal, never whatever the tip
|
|
5219
|
+
// happens to be (item 51).
|
|
5220
|
+
let tipSha = refExists ? await this.readMirrorSha(binding.ref).catch(() => null) : null;
|
|
5138
5221
|
let target = attachTarget({
|
|
5139
5222
|
refExists,
|
|
5140
5223
|
wantSha: want,
|
|
5141
5224
|
commitInMirror: !refExists && want !== null && (await this.commitInMirror(want)),
|
|
5225
|
+
tipSha,
|
|
5142
5226
|
});
|
|
5143
5227
|
if (target.kind === "unknown-ref" && returnable) {
|
|
5144
5228
|
// The thread's branch — one a rebind moved it onto, or one its own
|
|
@@ -5151,10 +5235,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5151
5235
|
binding = back.binding;
|
|
5152
5236
|
returned = back.returned;
|
|
5153
5237
|
want = wantShaForBinding({ boundRef: binding.ref, refHint, wantSha });
|
|
5238
|
+
const defaultExists = await this.refExists(binding.ref);
|
|
5239
|
+
tipSha = defaultExists ? await this.readMirrorSha(binding.ref).catch(() => null) : null;
|
|
5154
5240
|
target = attachTarget({
|
|
5155
|
-
refExists:
|
|
5241
|
+
refExists: defaultExists,
|
|
5156
5242
|
wantSha: want,
|
|
5157
5243
|
commitInMirror: false,
|
|
5244
|
+
tipSha,
|
|
5158
5245
|
});
|
|
5159
5246
|
}
|
|
5160
5247
|
if (target.kind === "unknown-ref") {
|
|
@@ -5164,7 +5251,19 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5164
5251
|
(want !== null ? `, and the mirror does not hold the expected commit ${want.slice(0, 7)} either` : ""),
|
|
5165
5252
|
);
|
|
5166
5253
|
}
|
|
5167
|
-
|
|
5254
|
+
if (target.kind === "stale-tip") {
|
|
5255
|
+
throw new StepError(
|
|
5256
|
+
"stale-tip",
|
|
5257
|
+
`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`,
|
|
5258
|
+
);
|
|
5259
|
+
}
|
|
5260
|
+
// A tip the read could not answer is never provisioned at: with no
|
|
5261
|
+
// commit named the target is `ref` without looking at the tip, so the
|
|
5262
|
+
// failed read is named here instead of becoming a null sha.
|
|
5263
|
+
const sha = target.kind === "sha" ? target.sha : tipSha;
|
|
5264
|
+
if (sha === null) {
|
|
5265
|
+
throw new StepError("rev-parse", `the mirror's tip of ${JSON.stringify(binding.ref)} could not be read`);
|
|
5266
|
+
}
|
|
5168
5267
|
const threadLockKey = await this.lockfileKey(sha);
|
|
5169
5268
|
const recreated = await this.ensureThreadWorktree(binding, sha, mode.originUrl, mode.modeSwitch, {
|
|
5170
5269
|
detached: target.kind === "sha",
|
|
@@ -5186,6 +5285,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5186
5285
|
if (err instanceof StepError && err.step === "unknown-ref") {
|
|
5187
5286
|
return { error: `unknown-ref: ${err.message}`, status: 400 };
|
|
5188
5287
|
}
|
|
5288
|
+
if (err instanceof StepError && err.step === "stale-tip") {
|
|
5289
|
+
// Item 51: not a resident fault and not a caller fault — a fact about
|
|
5290
|
+
// the mirror at this instant. 409 with the state, so the bot's named
|
|
5291
|
+
// fallback runs cold at the commit it asked for.
|
|
5292
|
+
const s = await this.getStatus();
|
|
5293
|
+
return { error: `stale-tip: ${err.message}`, status: 409, state: s.state, reason: "stale-tip" };
|
|
5294
|
+
}
|
|
5189
5295
|
return this.attachFailed(err);
|
|
5190
5296
|
}
|
|
5191
5297
|
|
|
@@ -6361,6 +6467,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6361
6467
|
evictedLeftBehind: tree && "leftBehind" in tree ? tree.leftBehind : undefined,
|
|
6362
6468
|
evictedUnmeasured: tree && "unmeasured" in tree ? tree.unmeasured : undefined,
|
|
6363
6469
|
} satisfies ThreadBinding);
|
|
6470
|
+
// The run's registration goes with its binding (item 44): released here,
|
|
6471
|
+
// never on a give-way above — a re-attach has re-registered it anyway.
|
|
6472
|
+
await this.ctx.storage.delete(runRegKey(binding.threadKey));
|
|
6364
6473
|
if (tree) console.log(`${logCtx}: ${binding.threadKey} evicted (${why}) — ${evictedTreeSentence(tree)}`);
|
|
6365
6474
|
return true;
|
|
6366
6475
|
}
|
|
@@ -6522,10 +6631,33 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6522
6631
|
return threadOps + this.opUsersInUse.size + this.attachesInFlight;
|
|
6523
6632
|
}
|
|
6524
6633
|
/** In-flight activity for the deploy preflight (GET /status, GET /residents).
|
|
6525
|
-
*
|
|
6526
|
-
* survived to be interrupted
|
|
6634
|
+
* The in-memory counters (a fresh isolate answers 0 for them — nothing of
|
|
6635
|
+
* THEIRS survived to be interrupted) plus the durable run registrations:
|
|
6636
|
+
* a harness run's process DOES survive an isolate swap in the container,
|
|
6637
|
+
* so its registration must too. */
|
|
6527
6638
|
async getInFlightCount(): Promise<number> {
|
|
6528
|
-
return this.inFlightCount();
|
|
6639
|
+
return this.inFlightCount() + (await this.registeredRunsBeyondOps());
|
|
6640
|
+
}
|
|
6641
|
+
/** A run holds its worktree from `/attach` to its release (`/detach`, the
|
|
6642
|
+
* sweep, disk pressure — every path ends in `evictBinding`, which clears
|
|
6643
|
+
* the row). Written by the attach; idempotent, a re-attach refreshes it. */
|
|
6644
|
+
private async registerRun(threadKey: string): Promise<void> {
|
|
6645
|
+
await this.ctx.storage.put(runRegKey(threadKey), {
|
|
6646
|
+
threadKey,
|
|
6647
|
+
registeredAt: new Date(systemClock()).toISOString(),
|
|
6648
|
+
} satisfies RunRegistration);
|
|
6649
|
+
}
|
|
6650
|
+
/** The registrations the op counters do not already see: a thread with an
|
|
6651
|
+
* op in flight is counted by `runsInFlightCount`, so its registration is
|
|
6652
|
+
* not counted again. Deliberately NOT part of `inFlightCount()`: the
|
|
6653
|
+
* container-lifecycle predicates (isIdle, reconcileImage) must not let a
|
|
6654
|
+
* stale registration pin a container awake — the clean-idle sweep is what
|
|
6655
|
+
* drains a registration whose release never came. */
|
|
6656
|
+
private async registeredRunsBeyondOps(): Promise<number> {
|
|
6657
|
+
const regs = await this.ctx.storage.list<RunRegistration>({ prefix: RUN_REG_KEY_PREFIX });
|
|
6658
|
+
let n = 0;
|
|
6659
|
+
for (const r of regs.values()) if ((this.threadOpsInFlight.get(r.threadKey) ?? 0) === 0) n++;
|
|
6660
|
+
return n;
|
|
6529
6661
|
}
|
|
6530
6662
|
private attachesInFlight = 0;
|
|
6531
6663
|
/** A refresh cycle past its idle/reconcile gates (fetching, rebuilding, snapshotting). */
|
|
@@ -7068,6 +7200,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7068
7200
|
REFRESH_INSTANCE_KEY,
|
|
7069
7201
|
RUNTIME_UNREACHABLE_KEY,
|
|
7070
7202
|
AUTO_REBUILDS_KEY,
|
|
7203
|
+
INFRA_STREAK_KEY,
|
|
7071
7204
|
]);
|
|
7072
7205
|
const facts = map.get(FACTS_KEY) as RepoFacts | undefined;
|
|
7073
7206
|
const snap = map.get(SNAPSHOT_KEY) as SnapshotRecord | undefined;
|
|
@@ -7120,6 +7253,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7120
7253
|
recycledUnmeasured: recycledUnmeasured ?? null,
|
|
7121
7254
|
}),
|
|
7122
7255
|
);
|
|
7256
|
+
// The runs whose process lives in the container between operator calls
|
|
7257
|
+
// (item 44): what the deploy preflight must see, added to both counts.
|
|
7258
|
+
const registeredRuns = await this.registeredRunsBeyondOps();
|
|
7123
7259
|
return {
|
|
7124
7260
|
resource: map.get(RESOURCE_KEY) ?? null,
|
|
7125
7261
|
state: map.get(STATE_KEY) ?? "down",
|
|
@@ -7139,9 +7275,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7139
7275
|
provisionRun: provisionRun.length,
|
|
7140
7276
|
provisionDeadline: provisionDeadline.length,
|
|
7141
7277
|
},
|
|
7142
|
-
inFlight: this.inFlightCount(),
|
|
7278
|
+
inFlight: this.inFlightCount() + registeredRuns,
|
|
7143
7279
|
// The runs alone (no refresh cycle): what the deploy preflight refuses on.
|
|
7144
|
-
runsInFlight: this.runsInFlightCount(),
|
|
7280
|
+
runsInFlight: this.runsInFlightCount() + registeredRuns,
|
|
7145
7281
|
// Item 22: who holds what, as the rows say — the mirror mutex and the
|
|
7146
7282
|
// cycle/hydration leases, each judged against this incarnation.
|
|
7147
7283
|
incarnation: this.incarnation,
|
|
@@ -7173,6 +7309,12 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7173
7309
|
// Item 36: the auto-rebuilds this resident has had (ISO instants; the
|
|
7174
7310
|
// budget's window is judged over them). Empty after a person's rebuild.
|
|
7175
7311
|
autoRebuilds: (map.get(AUTO_REBUILDS_KEY) as string[] | undefined) ?? [],
|
|
7312
|
+
// Item 67: consecutive cycles failed in the resident's own steps, with
|
|
7313
|
+
// the rung that count is on; null while cycles complete.
|
|
7314
|
+
infraStreak: (() => {
|
|
7315
|
+
const row = map.get(INFRA_STREAK_KEY) as InfraStreakRow | undefined;
|
|
7316
|
+
return row ? { ...row, rung: infraStreakRung(row.count) } : null;
|
|
7317
|
+
})(),
|
|
7176
7318
|
};
|
|
7177
7319
|
}
|
|
7178
7320
|
|
|
@@ -7255,6 +7397,33 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7255
7397
|
return { recreated: true, restoreStartedAt };
|
|
7256
7398
|
}
|
|
7257
7399
|
|
|
7400
|
+
/** Fault injection for item 67: write the resident-step streak row at `count`
|
|
7401
|
+
* (0 deletes it), so the next real failure lands on the rung under test.
|
|
7402
|
+
* Test-only semantics; admin scope. */
|
|
7403
|
+
async debugSetInfraStreak(count: number): Promise<{ infraStreak: (InfraStreakRow & { rung: string }) | null }> {
|
|
7404
|
+
if (count === 0) {
|
|
7405
|
+
await this.ctx.storage.delete(INFRA_STREAK_KEY);
|
|
7406
|
+
return { infraStreak: null };
|
|
7407
|
+
}
|
|
7408
|
+
const now = new Date(systemClock()).toISOString();
|
|
7409
|
+
const row: InfraStreakRow = { step: "injected", count, firstAt: now, lastAt: now };
|
|
7410
|
+
await this.ctx.storage.put(INFRA_STREAK_KEY, row);
|
|
7411
|
+
return { infraStreak: { ...row, rung: infraStreakRung(count) } };
|
|
7412
|
+
}
|
|
7413
|
+
|
|
7414
|
+
/** Fault injection for item 67: corrupt the mirror on disk (its objects
|
|
7415
|
+
* removed), so the next cycle's `fetch` fails in a resident step. The
|
|
7416
|
+
* snapshots in R2 are untouched — the ladder's recreate restores them.
|
|
7417
|
+
* Test-only semantics; admin scope. */
|
|
7418
|
+
async debugBreakMirror(): Promise<{ broken: boolean; error?: string }> {
|
|
7419
|
+
try {
|
|
7420
|
+
await this.runOk(["rm", "-rf", `${MIRROR_DIR}/objects`], "break-mirror");
|
|
7421
|
+
return { broken: true };
|
|
7422
|
+
} catch (err) {
|
|
7423
|
+
return { broken: false, error: errMsg(err) };
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
|
|
7258
7427
|
/** Fault injection for item 36 without corrupting real R2 objects. Bare: persist
|
|
7259
7428
|
* `down` with a rehydration-flavored reason and nothing else, so the next
|
|
7260
7429
|
* watchdog pass is the backstop under test. `transition`: go through
|
|
@@ -8644,6 +8813,19 @@ async function handleDebug(env: Env, body: Record<string, unknown>): Promise<Res
|
|
|
8644
8813
|
}
|
|
8645
8814
|
case "force-onboarding":
|
|
8646
8815
|
return json(await stub.debugForceOnboarding());
|
|
8816
|
+
case "infra-streak": {
|
|
8817
|
+
// Fault injection for item 67: set the resident-step streak's count, so
|
|
8818
|
+
// one real failure lands on the rung under test (recreate at 3, down at 5).
|
|
8819
|
+
const count =
|
|
8820
|
+
typeof body.count === "number" && Number.isInteger(body.count) && body.count >= 0 ? body.count : null;
|
|
8821
|
+
if (count === null) return json({ error: "count must be a non-negative integer" }, 400);
|
|
8822
|
+
return json(await stub.debugSetInfraStreak(count));
|
|
8823
|
+
}
|
|
8824
|
+
case "break-mirror":
|
|
8825
|
+
// Fault injection for item 67: remove the mirror's objects, so the next
|
|
8826
|
+
// cycle's fetch fails in a resident step — a corrupt disk, which the
|
|
8827
|
+
// ladder's recreate restores from the snapshot. Test residents only.
|
|
8828
|
+
return json(await stub.debugBreakMirror());
|
|
8647
8829
|
case "force-down": {
|
|
8648
8830
|
// Fault injection for item 36; a rehydration-flavored default reason
|
|
8649
8831
|
// 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.242.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.242.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.242.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.242.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",
|
package/dist/assets/project.json
CHANGED
|
@@ -204,11 +204,11 @@
|
|
|
204
204
|
"when": "After a `web/` change."
|
|
205
205
|
},
|
|
206
206
|
"screenshots:gen": {
|
|
207
|
-
"does": "Renders the dashboard
|
|
207
|
+
"does": "Renders the dashboard screenshots whose inputs changed, both themes, recording each surface's input hashes in `docs/public/screenshots/manifest/` (`--force`: all).",
|
|
208
208
|
"when": "After a `web/` or fixture change, once `screenshots:check` names it; needs `npx playwright-core install chromium`, so it is not part of `fix`."
|
|
209
209
|
},
|
|
210
210
|
"screenshots:check": {
|
|
211
|
-
"does": "
|
|
211
|
+
"does": "Each surface's inputs still hash to what its screenshots were rendered from — no browser.",
|
|
212
212
|
"when": "Part of `check:consistency`."
|
|
213
213
|
},
|
|
214
214
|
"load": {
|
package/dist/assets/source.json
CHANGED