@coreplane/switchboard 1.251.0 → 1.253.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.
Files changed (167) hide show
  1. package/dist/assets/config/config.example.yaml +28 -2
  2. package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
  3. package/dist/assets/deploy/cloudflare/worker.ts +6 -3
  4. package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +223 -38
  6. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
  7. package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
  8. package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
  9. package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
  10. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
  11. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
  12. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
  13. package/dist/assets/deploy/profile.example.json +1 -1
  14. package/dist/assets/package-lock.json +3 -3
  15. package/dist/assets/package.json +3 -2
  16. package/dist/assets/project.json +13 -9
  17. package/dist/assets/source.json +3 -3
  18. package/dist/assets/src/agents/registry.ts +5 -5
  19. package/dist/assets/src/core/authz/policy.ts +8 -0
  20. package/dist/assets/src/core/budgets.ts +39 -0
  21. package/dist/assets/src/core/coordinator/contract.ts +93 -1
  22. package/dist/assets/src/core/coordinator/driver.ts +236 -21
  23. package/dist/assets/src/core/drain.ts +50 -0
  24. package/dist/assets/src/core/memory/engine.ts +123 -4
  25. package/dist/assets/src/core/memory/scorer.ts +12 -4
  26. package/dist/assets/src/core/memory/types.ts +73 -12
  27. package/dist/assets/src/core/modelCard.ts +32 -4
  28. package/dist/assets/src/core/modelPricing.ts +111 -1
  29. package/dist/assets/src/core/modelProxy/usage.ts +88 -0
  30. package/dist/assets/src/core/modelRegistry.ts +15 -1
  31. package/dist/assets/src/core/refusal.ts +4 -7
  32. package/dist/assets/src/core/reviewVerdict.ts +4 -0
  33. package/dist/assets/src/core/runEvents.ts +82 -3
  34. package/dist/assets/src/core/runFriction.ts +8 -2
  35. package/dist/assets/src/core/runLedger/types.ts +11 -0
  36. package/dist/assets/src/core/runMetrics.ts +237 -0
  37. package/dist/assets/src/core/runRecord.ts +46 -0
  38. package/dist/assets/src/core/runUsage.ts +67 -13
  39. package/dist/assets/src/core/schedules.ts +3 -0
  40. package/dist/assets/src/core/ship/contract.ts +34 -14
  41. package/dist/assets/src/core/ship/coordinator.ts +485 -55
  42. package/dist/assets/src/core/ship/renewal.ts +10 -5
  43. package/dist/assets/src/core/trace/attrs.ts +24 -0
  44. package/dist/assets/src/core/types.ts +5 -5
  45. package/dist/assets/src/core/verbosity.ts +48 -0
  46. package/dist/assets/src/deploy/liveGate.ts +40 -13
  47. package/dist/assets/src/deploy/profile.ts +16 -0
  48. package/dist/assets/src/deploy/restart.ts +11 -12
  49. package/dist/assets/src/execution/residentDepCache.ts +50 -1
  50. package/dist/assets/src/execution/residentDepsStore.ts +40 -2
  51. package/dist/assets/src/execution/residentRefresh.ts +55 -3
  52. package/dist/assets/src/execution/residentSteps.ts +4 -0
  53. package/dist/assets/src/execution/sandboxErrors.ts +8 -0
  54. package/dist/assets/web/dist/.vite/manifest.json +444 -430
  55. package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
  56. package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
  57. package/dist/assets/web/dist/assets/{DeliveryPage-DUXd-Sl-.js → DeliveryPage-C7jaEJip.js} +1 -1
  58. package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
  59. package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
  60. package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
  61. package/dist/assets/web/dist/assets/{PendingTurnRow-DDhMhrI7.js → PendingTurnRow-C-pUgyyX.js} +1 -1
  62. package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
  63. package/dist/assets/web/dist/assets/{ResidentDetailPage-BnEoOnGQ.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
  64. package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
  65. package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
  66. package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
  67. package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
  68. package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
  69. package/dist/assets/web/dist/assets/{ScheduledPage-B_GgeJrb.js → ScheduledPage-C8FYoZy5.js} +1 -1
  70. package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
  71. package/dist/assets/web/dist/assets/{SettingsPage-BXX4R113.js → SettingsPage-CkYIhLUl.js} +1 -1
  72. package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-C0TUgnC1.js} +1 -1
  73. package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-CN5cZdl3.js} +1 -1
  74. package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.js → UnitRoutePage-D_EtAl8r.js} +1 -1
  75. package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
  76. package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
  77. package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
  78. package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
  79. package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
  80. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
  81. package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
  82. package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
  83. package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
  84. package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
  85. package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
  86. package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
  87. package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
  88. package/dist/assets/web/dist/assets/{dist-BCVXeBJ9.js → dist-B1Mf2JbM.js} +2 -2
  89. package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
  90. package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
  91. package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
  92. package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
  93. package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
  94. package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
  95. package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
  96. package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
  97. package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
  98. package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
  99. package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
  100. package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
  101. package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
  102. package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
  103. package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
  104. package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
  105. package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
  106. package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
  107. package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
  108. package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
  109. package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
  110. package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
  111. package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
  112. package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
  113. package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
  114. package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
  115. package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
  116. package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
  117. package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
  118. package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
  119. package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
  120. package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
  121. package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
  122. package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
  123. package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
  124. package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
  125. package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
  126. package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
  127. package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
  128. package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
  129. package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
  130. package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
  131. package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
  132. package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
  133. package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
  134. package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
  135. package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
  136. package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
  137. package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
  138. package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
  139. package/dist/assets/web/dist/assets/sseReplay-BYo2EZKy.js +11 -0
  140. package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
  141. package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
  142. package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
  143. package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
  144. package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
  145. package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
  146. package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
  147. package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
  148. package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
  149. package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
  150. package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
  151. package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
  152. package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
  153. package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
  154. package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
  155. package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
  156. package/dist/cli.js +5623 -2455
  157. package/package.json +1 -1
  158. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
  159. package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
  160. package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
  161. package/dist/assets/web/dist/assets/RunRoutePage-9klVWhSF.js +0 -9
  162. package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
  163. package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
  164. package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
  165. package/dist/assets/web/dist/assets/main-Dkcbtu3u.js +0 -28
  166. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
  167. package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +0 -9
@@ -167,6 +167,12 @@ defaults:
167
167
  # verbosity: verbose # hear every acknowledgement of what the bot does for you
168
168
  # harness:
169
169
  # coding: opencode
170
+ # # The person's GitHub author binding (record 0062; authorization.md item 18):
171
+ # # the login their commits are authored as. A bare login here is resolved to
172
+ # # { login, id } once at first use; `config set user --user <id> --github
173
+ # # <login>` (identity:write holders) stores the pair. Never set by the person
174
+ # # themselves; `config clear me` keeps it. One login/id binds one person.
175
+ # github: octocat
170
176
 
171
177
  # Authorization (docs/reference/specs/authorization.md item 9, docs/reference/authorization.md):
172
178
  # two blocks. `restrict` names what is CLOSED unless granted — an agent listed
@@ -325,8 +331,10 @@ workspaceDir: ./workspaces
325
331
  # # and — when the run is bound to a repo — that repo's scope (repo:owner/name)
326
332
  # # plus the requesting user's own scope (user:slack:U…) — a
327
333
  # # person's records never surface for anyone else.
328
- # limit: 8 # max records retrieved/injected per request (default 8)
329
- # maxTokens: 800 # hard token budget for the injected block (default 800)
334
+ # limit: 32 # max records retrieved/injected per request (default 32)
335
+ # maxTokens: 3000 # hard token budget for the injected block (default 3000)
336
+ # repoWindow: 24 # newest repo facts injected ahead of the keyword hits when
337
+ # # the run is bound to a repo (default 24; 0 disables the window)
330
338
  # maxRecordsPerScope: 500
331
339
  # # per-scope cap on ACTIVE records (default 500): a write
332
340
  # # that would exceed it evicts the least recently used
@@ -386,6 +394,8 @@ workspaceDir: ./workspaces
386
394
  # ship:
387
395
  # maxRounds: 3 # review rounds per unit
388
396
  # maxMinutes: 240 # the ship preset's wall-clock budget in minutes (the registry's default)
397
+ # idleDays: 0 # days a ship unit idles instead of ending (record 0051): 0–365, 0 = today's
398
+ # # endings; also per channel/user (`channels.<id>.ship.idleDays`), user > channel > org
389
399
 
390
400
  # Which harness each preset's runs are driven by, deployment-wide
391
401
  # (docs/reference/specs/harness.md item 8): a mapping of preset to a harness's
@@ -452,10 +462,16 @@ workspaceDir: ./workspaces
452
462
  # `tool` (default) forces a call to the router's `route` tool, whose schema is
453
463
  # the answer, so prose cannot occur; `text` is the one-JSON-object text contract
454
464
  # alone — set it for a provider or model that cannot take a forced tool call.
465
+ # `operator` is the one door's flag (routing-and-config item 29): `off`
466
+ # (default) — the operator never runs; `shadow` — one operator turn per
467
+ # admitted chat event, ahead of stage A, its decision written beside the
468
+ # routed request in the run store and nothing run from it; `on` — the
469
+ # operator's decision is what runs. `shadow` runs even under `auto: false`.
455
470
  # routing:
456
471
  # auto: false
457
472
  # model: anthropic/claude-haiku-4-5
458
473
  # answer: text
474
+ # operator: shadow
459
475
 
460
476
  # The thread-reply intake gate (docs/decisions/0058-a-thread-reply-is-read-before-it-is-answered-intake-decides-whether-the-bot-was-addressed.md;
461
477
  # docs/reference/specs/routing-and-config.md item 27). How an unmentioned reply
@@ -517,6 +533,16 @@ workspaceDir: ./workspaces
517
533
  # cacheRead: 0.125 # never $0, on the costs page and on a run's own page
518
534
  # cacheWrite: 0
519
535
 
536
+ # Run metrics (docs/reference/specs/run-metrics.md). Optional. The Analytics Engine
537
+ # dataset every finished run's point lands in — the SAME name the deployment
538
+ # profile's `metrics.dataset` binds on the state Worker as RUN_METRICS (the bot
539
+ # warns at boot when the two differ; neither side refuses to start). The reader
540
+ # over the dataset — `metrics trend` and the /metrics page, behind the `metrics`
541
+ # capability — is later work of the same decision; until it lands, only `dataset`
542
+ # is read, for that boot comparison.
543
+ # metrics:
544
+ # dataset: switchboard_runs
545
+
520
546
  # Delivery indicators: GET /delivery and `delivery report` (docs/reference/specs/delivery.md).
521
547
  # Optional. Needs a GitHub credential (the App triple or GH_TOKEN); read-only on GitHub.
522
548
  # delivery:
@@ -1,23 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  // Deploy preflight for the bot Worker (docs/reference/specs/slack-channel.md item 8).
3
3
  //
4
- // `wrangler deploy` rolls the bot container. Cloudflare's rollout sends SIGTERM
5
- // and the container is gone at the drain's end, whatever it was still driving:
6
- // the run ledger's handoff (docs/reference/specs/run-history.md item 39) lets the
7
- // next generation resume a run, but a resume is a recovery, not a guarantee —
8
- // one that fails costs the run — so a deploy never rolls over a live run.
9
- // `npm run deploy` runs this first and refuses while
10
- // - the bot reports runs in flight (`GET /healthz` `inFlight > 0`): the
11
- // runner retries every minute up to its budget, then fails by name;
4
+ // `wrangler deploy` rolls the bot container. Cloudflare's rollout sends SIGTERM;
5
+ // since the run ledger's handoff (docs/reference/specs/run-history.md item 39) the bot
6
+ // hands every resumable run to the next generation and exits within seconds,
7
+ // and the next generation continues the runs under their own cards — so a
8
+ // deploy no longer waits on runs, and this preflight no longer refuses for
9
+ // them. `npm run deploy` runs it first and refuses only while
12
10
  // - the container application is not in a settled state (a rollout is still
13
11
  // provisioning/updating — `wrangler containers list --json`): a second
14
12
  // rollout on top of one in progress replaces the instance the first put
15
13
  // into its graceful drain and kills whatever it was running (two deploys
16
14
  // 90 s apart once killed a review at 153 s).
17
- // It WARNS (never refuses) when the bot is already draining with nothing in
18
- // flight (`draining: true` — the instance exits on its own), and when /healthz
19
- // says the reconnect catch-up is failing or the bot token lacks required
20
- // scopes (`catchUp.error`, `catchUp.missingScopes`).
15
+ // It WARNS (never refuses) when the bot reports runs in flight (`inFlight > 0`
16
+ // — they hand off) or is already draining (`draining: true` — its resumable
17
+ // runs were handed off; a ship pipeline still in flight would be killed), and
18
+ // when /healthz says the reconnect catch-up is failing or the bot token lacks
19
+ // required scopes (`catchUp.error`, `catchUp.missingScopes`).
21
20
  //
22
21
  // Fail closed: unreachable bot, a body without the JSON shape (a bot whose
23
22
  // /healthz answers a bare `ok` is not one this preflight can read), a wrangler failure, or an app
@@ -41,7 +40,7 @@ export const APP_NAME = "switchboard-switchboardserver";
41
40
  const SETTLED_APP_STATES = new Set(["active", "ready"]);
42
41
 
43
42
  const HOW_TO_FORCE =
44
- "to deploy anyway (over the runs in flight — this kills them —, over a rollout in progress, or blind when the bot cannot be consulted): `SWITCHBOARD_DEPLOY_FORCE=1 npm run deploy` (`node preflight.mjs --force` checks alone)";
43
+ "to deploy anyway (over a rollout in progress, or blind when the bot cannot be consulted): `SWITCHBOARD_DEPLOY_FORCE=1 npm run deploy` (`node preflight.mjs --force` checks alone)";
45
44
 
46
45
  /** GET /healthz. Never throws: `{ok:true,payload}` (parsed JSON, or the raw text when not JSON) or `{ok:false,error}`. */
47
46
  export async function fetchHealth(baseUrl, { timeoutMs = 20_000 } = {}) {
@@ -165,16 +164,15 @@ export function decide({ health, apps }, { force = false } = {}) {
165
164
  if (!Number.isInteger(p.inFlight) || p.inFlight < 0) {
166
165
  problems.push(`bot reports an impossible inFlight=${JSON.stringify(p.inFlight)} (counter bug or old Worker)`);
167
166
  } else if (p.inFlight > 0) {
168
- // A refusal: the rollout rolls the container under these runs. A
169
- // handoff (run-history item 39) may resume them on the next generation,
170
- // but a resume that fails costs the run — the deploy waits instead.
171
- problems.push(
172
- `${p.inFlight} run(s) in flight — the rollout would roll the bot container under them (a handoff is a recovery, not a guarantee)`,
167
+ // Not a refusal since the handoff (run-history item 39): SIGTERM hands
168
+ // every resumable run to the next generation, which continues it.
169
+ warnings.push(
170
+ `${p.inFlight} run(s) in flight — handed to the next generation on SIGTERM (run-history item 39); they continue there under their own cards`,
173
171
  );
174
172
  }
175
173
  if (p.draining === true) {
176
174
  warnings.push(
177
- "bot is already draining from a previous deploy — the draining instance exits on its own; this rollout replaces it at once",
175
+ "bot is already draining from a previous deploy — its resumable runs are handed off; a ship pipeline still in flight would be killed when this rollout replaces the draining instance",
178
176
  );
179
177
  }
180
178
  }
@@ -211,7 +209,7 @@ export function decide({ health, apps }, { force = false } = {}) {
211
209
  forced: true,
212
210
  problems,
213
211
  warnings,
214
- message: `preflight WARNING: deploying by force despite —\n${detail}\n this WILL kill the runs in flight that no resume recovers, and a rollout landing on one in progress can disrupt it${warningText}`,
212
+ message: `preflight WARNING: deploying by force despite —\n${detail}\n a rollout landing on one in progress can disrupt it; in-flight runs hand off regardless (run-history item 39)${warningText}`,
215
213
  };
216
214
  }
217
215
  return {
@@ -219,7 +217,7 @@ export function decide({ health, apps }, { force = false } = {}) {
219
217
  forced: false,
220
218
  problems,
221
219
  warnings,
222
- message: `preflight REFUSED: a Worker deploy rolls the bot container —\n${detail}\n wait for them to finish and retry; ${HOW_TO_FORCE}${warningText}`,
220
+ message: `preflight REFUSED: a Worker deploy rolls the bot container —\n${detail}\n wait and retry; ${HOW_TO_FORCE}${warningText}`,
223
221
  };
224
222
  }
225
223
 
@@ -197,8 +197,10 @@ export class SwitchboardServer extends Container<Env> {
197
197
  * and exits, and the NEXT request through `fetch` starts the container again
198
198
  * (`startBot`, env computed then). The keep-alive cron GETs /healthz every
199
199
  * minute and the CLI's live gate polls it every 15 s, so the next request is
200
- * never more than seconds away. Refuses (the deploy preflight's rules) while
201
- * runs are in flight or a drain is already under way unless `force`.
200
+ * never more than seconds away. Refuses (the deploy preflight's rules) only the
201
+ * fail-closed cases — no JSON body, an impossible `inFlight` — unless
202
+ * `force`; runs in flight or a drain under way warn and the stop proceeds
203
+ * (the handoff rule).
202
204
  */
203
205
  async restart(opts: { force: boolean }): Promise<RestartOutcome> {
204
206
  if (!this.ctx.container?.running) return { kind: "not-running" };
@@ -265,7 +267,8 @@ export class SwitchboardServer extends Container<Env> {
265
267
  * `deploy:write` in the bot's config). The Worker authenticates the bearer
266
268
  * against the map it holds — an unknown bearer never touches the container —
267
269
  * and the Container DO asks the bot for the grant before stopping anything.
268
- * Body `{ "force": true }` bypasses the in-flight/draining refusal. */
270
+ * Body `{ "force": true }` bypasses the fail-closed refusals (no JSON body,
271
+ * impossible `inFlight`); runs in flight or a drain warn and never refuse. */
269
272
  async function handleAdminRestart(request: Request, env: Env): Promise<Response> {
270
273
  const json = (status: number, body: Record<string, unknown>) =>
271
274
  new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -0,0 +1,31 @@
1
+ import type { RunMetricsPoint } from "../../src/core/runMetrics.ts";
2
+
3
+ // The run-metrics sink (docs/reference/specs/run-metrics.md): where the
4
+ // RunHistoryDO writes the one point of a run whose row turned final, after the
5
+ // record's commit. Two implementations behind one seam (AGENTS.md invariant 2):
6
+ // the Workers Analytics Engine dataset when the Worker is deployed with the
7
+ // optional `RUN_METRICS` binding, the null sink otherwise — so a Worker without
8
+ // the binding behaves byte-identically, the same shape as `SHIP_COORDINATOR?`.
9
+ // The write is advisory on the caller's side too: one try, one console.warn,
10
+ // never a failed put or finish.
11
+
12
+ export interface RunMetricsSink {
13
+ write(point: RunMetricsPoint): void;
14
+ }
15
+
16
+ /** The dataset the deploy bound: `writeDataPoint` is synchronous and does not
17
+ * throw on a platform-side drop; the caller's try covers a missing or
18
+ * misconfigured binding. */
19
+ export class AnalyticsEngineSink implements RunMetricsSink {
20
+ constructor(private readonly dataset: AnalyticsEngineDataset) {}
21
+ write(point: RunMetricsPoint): void {
22
+ this.dataset.writeDataPoint({ indexes: point.indexes, blobs: point.blobs, doubles: point.doubles });
23
+ }
24
+ }
25
+
26
+ /** No binding: points are dropped — the off state. */
27
+ export class NullSink implements RunMetricsSink {
28
+ write(): void {
29
+ // nothing is written without the binding
30
+ }
31
+ }