@coreplane/switchboard 1.240.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 +40 -8
- package/dist/assets/deploy/cloudflare/worker.ts +77 -18
- package/dist/assets/deploy/cloudflare-resident/worker.ts +198 -15
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +38 -5
- package/dist/assets/deploy/secrets.manifest.json +6 -0
- 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 +58 -22
- package/dist/assets/src/core/authz/actor.ts +17 -9
- package/dist/assets/src/core/authz/authorize.ts +17 -4
- package/dist/assets/src/core/authz/grants.ts +20 -9
- package/dist/assets/src/core/authz/types.ts +12 -0
- package/dist/assets/src/core/coordinator/contract.ts +40 -1
- package/dist/assets/src/core/coordinator/driver.ts +17 -1
- package/dist/assets/src/core/coordinator/instancesRoute.ts +72 -0
- package/dist/assets/src/core/costs.ts +22 -9
- 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/core/ship/coordinator.ts +22 -7
- package/dist/assets/src/core/trace/attrs.ts +1 -1
- package/dist/assets/src/execution/bindingPurge.ts +2 -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 +435 -439
- package/dist/assets/web/dist/assets/AppShell-IDMGG6yi.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-BD3w5j_S.js → CostsPage-CtmKhhOF.js} +2 -2
- package/dist/assets/web/dist/assets/{DeliveryPage-BtvzKA1m.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-DwOX1HaZ.js → NotFoundPage-C97EhJcQ.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CUAOh5d9.js → ResidentDetailPage-DTMBgnIW.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-B3KUlYy2.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-BnkvCBrM.js → RunsTabs-BUfdk0lH.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-C1y9jgAk.js → ScheduledPage-DXD2gLJk.js} +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-CiByEtoR.js → StatusDot-ELoXHlFt.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DyRxk8A8.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-DAQ_1T1v.js → angular-html-oBNfPJR0.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-D4kxRVp1.js → angular-ts-BvNwsyWA.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-Dy07l64y.js → apl-CNUdRlYf.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DZIbTlc7.js → astro-Zb0NriSe.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-CmSXWX3l.js → blade-qPRVheqq.js} +1 -1
- package/dist/assets/web/dist/assets/{c-ggzVdJLK.js → c-D8Awx4YO.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bo1wktsG.js → chapel-Bt72Mhsx.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DJUmtKM2.js → cobol-BOBacexg.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-BZWOVz4C.js → coffee-E4u0liHW.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-Dtk39yMw.js → cpp-q2sLNlul.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CGf_-Zoj.js → crystal-DyWqUnlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CX2fcL4u.js → css-CQY0hFsD.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-D8TaC961.js → dist-BU5UivXC.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-BobbycC-.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BYS8FtgQ.js → edge-C1MwhJkX.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-fhUpi3nO.js → elixir-Bb3YbHfn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAQNiwsN.js → elm-DAN9IGQw.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CWzEys6U.js → erb-BEB8Xlsj.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-9rY3p-Rw.js → git-rebase-tqpjRfxO.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-tY7hy0nE.js → glimmer-js-Ccbo65zR.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CTrLv5JF.js → glimmer-ts-B6WBVMpE.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-BRJbiDfq.js → glsl-tRec3Fcu.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-B-MtVLh2.js → graphql-P8kbxT4F.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BtIkOypQ.js → hack-H9Zhkagy.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CoOH6gfa.js → haml-DtEnpn7Z.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DgrRu-GP.js → handlebars-DkgPfoAz.js} +1 -1
- package/dist/assets/web/dist/assets/{html-DZaaI7ca.js → html-D30RXpIs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DgHvT-qZ.js → html-derivative-DwozLrEx.js} +1 -1
- package/dist/assets/web/dist/assets/{http-CvAd51yz.js → http-DXuzBAPm.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CXGqAfzx.js → hurl-d1UUJIt_.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-Cse3JGb0.js → indexRow-BD1VT8o8.js} +1 -1
- package/dist/assets/web/dist/assets/{java-CIQStIXF.js → java-DL0gWf34.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-WbH8s7E3.js → javascript-Cl7vavnS.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-DzyWEVKC.js → jinja-CfQOWMX9.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-CrT-zCa-.js → jison-KdYirlqm.js} +1 -1
- package/dist/assets/web/dist/assets/{json-lHjpjXMQ.js → json-C9cDQ-Qj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-WTwYCHW6.js → jsx-CRx5NItd.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-Bqpk9H7E.js → julia-CmsQsZQl.js} +1 -1
- package/dist/assets/web/dist/assets/{just-LdXk1r3m.js → just-CsM3Q8TE.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-h_y2A5KD.js → latex-BXCh5YRX.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CY7bF2_p.js → liquid-BF2vwK8p.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-oQ7XVZOY.js → lua-DcMBATrl.js} +1 -1
- package/dist/assets/web/dist/assets/main-B2fX10aW.css +1 -0
- package/dist/assets/web/dist/assets/{main-CGO6fQ8k.js → main-D4EA1g6n.js} +2 -2
- package/dist/assets/web/dist/assets/{marko-zv_qZ4QV.js → marko-88MndKvG.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-Dl4kL_YK.js → mdc-DJ4kVAd8.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CAHCZj4F.js → nginx-CP6mRgtV.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BOtmwCJH.js → nim-QQfj3fpF.js} +1 -1
- package/dist/assets/web/dist/assets/{org-DspQk3HQ.js → org-Bke3eBzc.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-CAXmvVl0.js → perl-Bkh0N7Kz.js} +1 -1
- package/dist/assets/web/dist/assets/{php-CMUx4VyM.js → php-Dnv1Piya.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-B55xVJTv.js → pug-D6peFFI7.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-DsNKCCRk.js → qml-D8PEs-C-.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DbhP0VvO.js → r-B1EL9b_j.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BCpE6P-p.js → razor-BsMTIh2b.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-BllsjiN4.js → regexp-HhvC8spD.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-CO5Rpq9V.js → rst-D5paAxpg.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-C2A4OgQq.js → ruby-BrQwhLrl.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-Butzoyql.js → sas-afot2B1o.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-PEUwJBpf.js → scss-Efm-mwuG.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-3Yhq0khE.js → shellscript-BK0Vv5fT.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-04Hkb7m3.js → shellsession-BGqlMC7N.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-BnId73cP.js → soy-M0b4UwGM.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-BKwdc0ch.js → sql-sxe6IE9j.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-g7ml86LM.js +9 -0
- package/dist/assets/web/dist/assets/{stata-BPPtNkay.js → stata-nPF_ddLP.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CDPChcYP.js → surrealql-D_GrC6u7.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-B15DIQ-z.js → svelte-DwL1AtNP.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-D-rp_AiB.js → templ-7s7LTDkc.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BnVDCD7p.js → tex-Bx-5fMxe.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-cpujdyAR.js → ts-tags-DUMJnke_.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-D1vO4L72.js → tsx-BN8biPDe.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-oQGnQGRc.js → twig-8nIu84TN.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-CKSFpXiH.js → typescript-BooSPq_S.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-DKHFo8BF.js → typst-CTBiBsem.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-BstokpiS.js → vue-C6Ft4Lea.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DpCp0_Sz.js → vue-html-Ba36dD5D.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-y5d5wUb1.js → vue-vine-NkFexVo2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-Br2hx5Ax.js → xml-C_THnHXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bc8Cobap.js → xsl-D8G5xqjY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-LOITy_I1.js → yaml-jAMIJzge.js} +1 -1
- package/dist/cli.js +4632 -817
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-CM_fh-WW.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-BU6aRCSe.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-Dulcbg25.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-DGoGrQZq.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-37W7OZbC.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-ggfB-Rqr.js +0 -1
- package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +0 -1
- package/dist/assets/web/dist/assets/Slideover-CX-LEklF.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CnWyOizV.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +0 -1
- package/dist/assets/web/dist/assets/main-O8UXRuEv.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-DutHNghE.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,13 +333,25 @@ 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
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
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.
|
|
352
|
+
# harness:
|
|
353
|
+
# coding: opencode # coding runs on OpenCode
|
|
354
|
+
# review: pi # the default, written out
|
|
337
355
|
|
|
338
356
|
# What the harness writes into pi's per-run settings for every run on pi
|
|
339
357
|
# (docs/reference/specs/harness-pi.md item 4): pi's compaction thresholds, in
|
|
@@ -349,6 +367,18 @@ workspaceDir: ./workspaces
|
|
|
349
367
|
# reserveTokens: 150000
|
|
350
368
|
# keepRecentTokens: 8000
|
|
351
369
|
|
|
370
|
+
# What the harness writes into OpenCode's per-run configuration for every run
|
|
371
|
+
# on OpenCode (docs/reference/specs/harness.md item 8): OpenCode's compaction
|
|
372
|
+
# thresholds, in tokens, under OpenCode's own names — `buffer`, the tokens kept
|
|
373
|
+
# free of the model's window before a compaction runs; `keepTokens`, how much
|
|
374
|
+
# of the newest turns it keeps. Unset, OpenCode's defaults stand and the file
|
|
375
|
+
# names no compaction. The OpenCode twin of the `pi` block above; positive
|
|
376
|
+
# integers only, any other shape or key fails the load by name.
|
|
377
|
+
# opencode:
|
|
378
|
+
# compaction:
|
|
379
|
+
# buffer: 20000
|
|
380
|
+
# keepTokens: 8000
|
|
381
|
+
|
|
352
382
|
# The fan-out cap a spawning run meets (docs/reference/specs/agent-conductor.md).
|
|
353
383
|
# `agent:conductor` starts child runs as the person who asked — each an
|
|
354
384
|
# ordinary run in a thread of its own, under that person's permissions — and
|
|
@@ -392,6 +422,8 @@ workspaceDir: ./workspaces
|
|
|
392
422
|
# snapshot: # the page serves a snapshot of both billing sources, never a live read;
|
|
393
423
|
# everyHours: 24 # how often the bot reads them (1–168, default 24). `costs snapshot`
|
|
394
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
|
|
395
427
|
# groups:
|
|
396
428
|
# myapp:
|
|
397
429
|
# label: My App
|
|
@@ -17,8 +17,11 @@ import {
|
|
|
17
17
|
createInstanceResponse,
|
|
18
18
|
instanceStatusResponse,
|
|
19
19
|
isInstanceNotFound,
|
|
20
|
+
parseInstanceEventPath,
|
|
20
21
|
parseInstanceStatusPath,
|
|
21
22
|
parseCreateInstanceRequest,
|
|
23
|
+
parseSendEventRequest,
|
|
24
|
+
sendEventResponse,
|
|
22
25
|
parseSubjectAuthorization,
|
|
23
26
|
type CreateInstanceOutcome,
|
|
24
27
|
} from "../../src/core/coordinator/instancesRoute.ts";
|
|
@@ -87,6 +90,7 @@ export interface Env {
|
|
|
87
90
|
DASHBOARD_TOKEN?: string; // dashboard auth `token` strategy: the bearer (the default env name; config may name another)
|
|
88
91
|
SWITCHBOARD_INGRESS_TOKENS?: string; // enables HTTP /ingress + MCP /mcp (JSON token→identity map); the `cron` entry is what scheduled runs present; an entry whose `http:<subject>` actor holds `deploy:write` in the bot's config may POST /admin/restart
|
|
89
92
|
BRAVE_SEARCH_API_KEY?: string; // web_search backend (Brave); web_fetch works without it
|
|
93
|
+
GITHUB_WEBHOOK_SECRET?: string; // check-run intake: signs POST /webhooks/github; absent, the intake answers 503 disabled
|
|
90
94
|
CF_ANALYTICS_TOKEN?: string; // costs dash: Cloudflare API token, Account Analytics:Read only
|
|
91
95
|
ANTHROPIC_ADMIN_KEY?: string; // costs dash (optional): Anthropic Admin API key for the LLM cost report
|
|
92
96
|
MEMORY_TOKEN?: string; // durable memory + friction ledger + schedule firings + MCP registry: bearer for the state Worker
|
|
@@ -125,6 +129,7 @@ const FORWARDED_OPTIONAL = [
|
|
|
125
129
|
"ANTHROPIC_ADMIN_KEY",
|
|
126
130
|
"SWITCHBOARD_INGRESS_TOKENS",
|
|
127
131
|
"BRAVE_SEARCH_API_KEY",
|
|
132
|
+
"GITHUB_WEBHOOK_SECRET",
|
|
128
133
|
"MEMORY_TOKEN",
|
|
129
134
|
"MCP_CREDENTIAL_KEY",
|
|
130
135
|
"MCP_ACCESS_CLIENT_ID",
|
|
@@ -407,6 +412,57 @@ async function handleCoordinatorInstanceStatus(request: Request, env: Env, id: s
|
|
|
407
412
|
return json(res.status, res.body);
|
|
408
413
|
}
|
|
409
414
|
|
|
415
|
+
/** `POST /admin/coordinator/instances/<id>/events` — the shim's event relay
|
|
416
|
+
* (docs/reference/specs/http-ingress.md item 12): the bot's check-run intake
|
|
417
|
+
* holds no Workflow binding, so its `checks-settled-<head>` send crosses here
|
|
418
|
+
* and this Worker's own `SHIP_COORDINATOR` binding delivers it. The same door
|
|
419
|
+
* as the create and the status: the bearer in the map, the bot's `authorize`
|
|
420
|
+
* answer. The engine's `instance.not_found` is 404; any other refusal (an
|
|
421
|
+
* instance that already ended) is 502 by reason — the sender treats both as a
|
|
422
|
+
* failed send and the driver's bounded wait is the fallback. */
|
|
423
|
+
async function handleCoordinatorInstanceEvent(request: Request, env: Env, id: string): Promise<Response> {
|
|
424
|
+
const json = (status: number, body: Record<string, unknown>) =>
|
|
425
|
+
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
426
|
+
if (request.method !== "POST")
|
|
427
|
+
return json(405, {
|
|
428
|
+
ok: false,
|
|
429
|
+
error: `method not allowed: ${request.method} — ${COORDINATOR_INSTANCES_PATH}/<id>/events answers POST only`,
|
|
430
|
+
});
|
|
431
|
+
const authorization = request.headers.get("authorization") ?? undefined;
|
|
432
|
+
const authn = authenticateIngressBearer(authorization, env.SWITCHBOARD_INGRESS_TOKENS, "coordinator");
|
|
433
|
+
if (!authn.ok) {
|
|
434
|
+
console.warn(`[coordinator] instance event ${authn.status} — ${authn.reason}`);
|
|
435
|
+
return json(authn.status, { ok: false, error: authn.reason });
|
|
436
|
+
}
|
|
437
|
+
let answer: Response;
|
|
438
|
+
try {
|
|
439
|
+
answer = await getContainer(env.SWITCHBOARD, INSTANCE).fetch(
|
|
440
|
+
new Request(`${INTERNAL}${COORDINATOR_AUTHORIZE_PATH}`, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: { authorization: authorization ?? "" },
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
447
|
+
return json(503, { ok: false, error: `coordinator disabled: the bot could not be asked (${reason})` });
|
|
448
|
+
}
|
|
449
|
+
const auth = parseSubjectAuthorization(answer.status, await answer.text().catch(() => ""));
|
|
450
|
+
if (!auth.ok) return json(auth.status, { ok: false, error: auth.reason });
|
|
451
|
+
const parsed = parseSendEventRequest(await request.text().catch(() => ""));
|
|
452
|
+
if (!parsed.ok) return json(400, { ok: false, error: parsed.reason });
|
|
453
|
+
let outcome: Parameters<typeof sendEventResponse>[0];
|
|
454
|
+
try {
|
|
455
|
+
await (await env.SHIP_COORDINATOR.get(id)).sendEvent({ type: parsed.type, payload: parsed.payload });
|
|
456
|
+
outcome = { kind: "sent", id };
|
|
457
|
+
} catch (err) {
|
|
458
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
459
|
+
outcome = isInstanceNotFound(reason) ? { kind: "absent", id } : { kind: "failed", id, reason };
|
|
460
|
+
}
|
|
461
|
+
const res = sendEventResponse(outcome);
|
|
462
|
+
console.log(`[coordinator] ${auth.subject} → instance ${id} event ${parsed.type}: ${outcome.kind}`);
|
|
463
|
+
return json(res.status, res.body);
|
|
464
|
+
}
|
|
465
|
+
|
|
410
466
|
/** Record a firing on the state Worker's ScheduleDO (the /runs Scheduled panel
|
|
411
467
|
* reads it). Best-effort: a failure here is a log line — the run itself (if
|
|
412
468
|
* any) already happened and is its own record. */
|
|
@@ -440,29 +496,32 @@ export default {
|
|
|
440
496
|
// The routes the Worker answers itself — the restart, the coordinator's
|
|
441
497
|
// instance creation and an instance's status, all over bindings only this
|
|
442
498
|
// Worker holds; everything else is the container's.
|
|
443
|
-
const
|
|
499
|
+
const eventsId = parseInstanceEventPath(pathname);
|
|
500
|
+
const statusId = eventsId === undefined ? parseInstanceStatusPath(pathname) : undefined;
|
|
444
501
|
const res =
|
|
445
502
|
pathname === "/admin/restart"
|
|
446
503
|
? await handleAdminRestart(forwarded, env)
|
|
447
504
|
: pathname === COORDINATOR_INSTANCES_PATH
|
|
448
505
|
? await handleCoordinatorInstances(forwarded, env)
|
|
449
|
-
:
|
|
450
|
-
? await
|
|
451
|
-
:
|
|
452
|
-
?
|
|
453
|
-
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
506
|
+
: eventsId !== undefined
|
|
507
|
+
? await handleCoordinatorInstanceEvent(forwarded, env, eventsId)
|
|
508
|
+
: statusId !== undefined
|
|
509
|
+
? await handleCoordinatorInstanceStatus(forwarded, env, statusId)
|
|
510
|
+
: pathname === COPY_PATH
|
|
511
|
+
? // The artifact copy (artifactsCopy.ts): the R2 binding and the Slack
|
|
512
|
+
// token are this Worker's; the bot only asks, with its copy bearer.
|
|
513
|
+
// `forwarded`, never `inbound`: `withTraceContext` rebuilt the request
|
|
514
|
+
// with `new Request(inbound, …)`, which takes the body stream with it —
|
|
515
|
+
// `inbound.text()` is empty afterwards and the route read "not JSON" live.
|
|
516
|
+
await handleArtifactsCopy(forwarded, {
|
|
517
|
+
bucket: env.ARTIFACTS,
|
|
518
|
+
bucketName: env.ARTIFACTS_BUCKET_NAME,
|
|
519
|
+
copyToken: env.ARTIFACTS_COPY_TOKEN,
|
|
520
|
+
slackToken: env.SLACK_BOT_TOKEN,
|
|
521
|
+
fetch: (input, init) => fetch(input, init),
|
|
522
|
+
lengthPipe: (size) => new FixedLengthStream(size),
|
|
523
|
+
})
|
|
524
|
+
: withLength(await getContainer(env.SWITCHBOARD, INSTANCE).fetch(forwarded));
|
|
466
525
|
root.end(res.status >= 500 ? "error" : "ok", { httpStatus: res.status });
|
|
467
526
|
return res;
|
|
468
527
|
} catch (err) {
|
|
@@ -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). */
|
|
@@ -7067,6 +7199,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7067
7199
|
LIFECYCLE_KEY,
|
|
7068
7200
|
REFRESH_INSTANCE_KEY,
|
|
7069
7201
|
RUNTIME_UNREACHABLE_KEY,
|
|
7202
|
+
AUTO_REBUILDS_KEY,
|
|
7203
|
+
INFRA_STREAK_KEY,
|
|
7070
7204
|
]);
|
|
7071
7205
|
const facts = map.get(FACTS_KEY) as RepoFacts | undefined;
|
|
7072
7206
|
const snap = map.get(SNAPSHOT_KEY) as SnapshotRecord | undefined;
|
|
@@ -7119,6 +7253,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7119
7253
|
recycledUnmeasured: recycledUnmeasured ?? null,
|
|
7120
7254
|
}),
|
|
7121
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();
|
|
7122
7259
|
return {
|
|
7123
7260
|
resource: map.get(RESOURCE_KEY) ?? null,
|
|
7124
7261
|
state: map.get(STATE_KEY) ?? "down",
|
|
@@ -7138,9 +7275,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7138
7275
|
provisionRun: provisionRun.length,
|
|
7139
7276
|
provisionDeadline: provisionDeadline.length,
|
|
7140
7277
|
},
|
|
7141
|
-
inFlight: this.inFlightCount(),
|
|
7278
|
+
inFlight: this.inFlightCount() + registeredRuns,
|
|
7142
7279
|
// The runs alone (no refresh cycle): what the deploy preflight refuses on.
|
|
7143
|
-
runsInFlight: this.runsInFlightCount(),
|
|
7280
|
+
runsInFlight: this.runsInFlightCount() + registeredRuns,
|
|
7144
7281
|
// Item 22: who holds what, as the rows say — the mirror mutex and the
|
|
7145
7282
|
// cycle/hydration leases, each judged against this incarnation.
|
|
7146
7283
|
incarnation: this.incarnation,
|
|
@@ -7172,6 +7309,12 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7172
7309
|
// Item 36: the auto-rebuilds this resident has had (ISO instants; the
|
|
7173
7310
|
// budget's window is judged over them). Empty after a person's rebuild.
|
|
7174
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
|
+
})(),
|
|
7175
7318
|
};
|
|
7176
7319
|
}
|
|
7177
7320
|
|
|
@@ -7254,6 +7397,33 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7254
7397
|
return { recreated: true, restoreStartedAt };
|
|
7255
7398
|
}
|
|
7256
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
|
+
|
|
7257
7427
|
/** Fault injection for item 36 without corrupting real R2 objects. Bare: persist
|
|
7258
7428
|
* `down` with a rehydration-flavored reason and nothing else, so the next
|
|
7259
7429
|
* watchdog pass is the backstop under test. `transition`: go through
|
|
@@ -8643,6 +8813,19 @@ async function handleDebug(env: Env, body: Record<string, unknown>): Promise<Res
|
|
|
8643
8813
|
}
|
|
8644
8814
|
case "force-onboarding":
|
|
8645
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());
|
|
8646
8829
|
case "force-down": {
|
|
8647
8830
|
// Fault injection for item 36; a rehydration-flavored default reason
|
|
8648
8831
|
// makes it eligible. `transition: true` fires the down through goDown
|