@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.
- package/dist/assets/config/config.example.yaml +28 -2
- package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
- package/dist/assets/deploy/cloudflare/worker.ts +6 -3
- package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +223 -38
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
- package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
- package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
- package/dist/assets/deploy/profile.example.json +1 -1
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +3 -2
- package/dist/assets/project.json +13 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +5 -5
- package/dist/assets/src/core/authz/policy.ts +8 -0
- package/dist/assets/src/core/budgets.ts +39 -0
- package/dist/assets/src/core/coordinator/contract.ts +93 -1
- package/dist/assets/src/core/coordinator/driver.ts +236 -21
- package/dist/assets/src/core/drain.ts +50 -0
- package/dist/assets/src/core/memory/engine.ts +123 -4
- package/dist/assets/src/core/memory/scorer.ts +12 -4
- package/dist/assets/src/core/memory/types.ts +73 -12
- package/dist/assets/src/core/modelCard.ts +32 -4
- package/dist/assets/src/core/modelPricing.ts +111 -1
- package/dist/assets/src/core/modelProxy/usage.ts +88 -0
- package/dist/assets/src/core/modelRegistry.ts +15 -1
- package/dist/assets/src/core/refusal.ts +4 -7
- package/dist/assets/src/core/reviewVerdict.ts +4 -0
- package/dist/assets/src/core/runEvents.ts +82 -3
- package/dist/assets/src/core/runFriction.ts +8 -2
- package/dist/assets/src/core/runLedger/types.ts +11 -0
- package/dist/assets/src/core/runMetrics.ts +237 -0
- package/dist/assets/src/core/runRecord.ts +46 -0
- package/dist/assets/src/core/runUsage.ts +67 -13
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +34 -14
- package/dist/assets/src/core/ship/coordinator.ts +485 -55
- package/dist/assets/src/core/ship/renewal.ts +10 -5
- package/dist/assets/src/core/trace/attrs.ts +24 -0
- package/dist/assets/src/core/types.ts +5 -5
- package/dist/assets/src/core/verbosity.ts +48 -0
- package/dist/assets/src/deploy/liveGate.ts +40 -13
- package/dist/assets/src/deploy/profile.ts +16 -0
- package/dist/assets/src/deploy/restart.ts +11 -12
- package/dist/assets/src/execution/residentDepCache.ts +50 -1
- package/dist/assets/src/execution/residentDepsStore.ts +40 -2
- package/dist/assets/src/execution/residentRefresh.ts +55 -3
- package/dist/assets/src/execution/residentSteps.ts +4 -0
- package/dist/assets/src/execution/sandboxErrors.ts +8 -0
- package/dist/assets/web/dist/.vite/manifest.json +444 -430
- package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-DUXd-Sl-.js → DeliveryPage-C7jaEJip.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
- package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-DDhMhrI7.js → PendingTurnRow-C-pUgyyX.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-BnEoOnGQ.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-B_GgeJrb.js → ScheduledPage-C8FYoZy5.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BXX4R113.js → SettingsPage-CkYIhLUl.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-C0TUgnC1.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-CN5cZdl3.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.js → UnitRoutePage-D_EtAl8r.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
- package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BCVXeBJ9.js → dist-B1Mf2JbM.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
- package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
- package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
- package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
- package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
- package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
- package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
- package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
- package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
- package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
- package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
- package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-BYo2EZKy.js +11 -0
- package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
- package/dist/cli.js +5623 -2455
- package/package.json +1 -1
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
- package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-9klVWhSF.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
- package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
- package/dist/assets/web/dist/assets/main-Dkcbtu3u.js +0 -28
- package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +0 -9
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
// progress off the row — never off a model's word — and renews only when three
|
|
5
5
|
// things hold: the row shows progress, the grant has a renewal left and the
|
|
6
6
|
// spend is under its cap, and the pipeline's ask still passes the fit.
|
|
7
|
-
// Otherwise it stops and names the clause that failed
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// Otherwise it stops and names the clause that failed. Nothing spends a renewal
|
|
8
|
+
// by hand today — follow-ups route by thread context, never by a keyword
|
|
9
|
+
// (routing-and-config item 3), and a re-issued request opens a fresh grant — so
|
|
10
|
+
// the stop says what actually spends one (a segment's progress) and to re-issue
|
|
11
|
+
// the request. Pure and node-free: the unit machine (a Worker) and the bot's
|
|
12
|
+
// routes compute the same decision from the same facts.
|
|
10
13
|
|
|
11
14
|
import { fit, type Grant, type Pipeline } from "../budgets.js";
|
|
12
15
|
import type { Handoff } from "./handoff.js";
|
|
@@ -140,7 +143,9 @@ export function renewalDecision(input: RenewalInput): RenewalDecision {
|
|
|
140
143
|
|
|
141
144
|
/** The card's words (the record's trace, steps 4 and 9): a renewal names its
|
|
142
145
|
* number of the grant's and the sha it continues from; a stop names the
|
|
143
|
-
* clause and, when renewals remain,
|
|
146
|
+
* clause and, when renewals remain, what actually spends one — a segment's
|
|
147
|
+
* progress — and the honest recourse (re-issue the request). It teaches no
|
|
148
|
+
* keyword: the router has none (routing-and-config item 3). */
|
|
144
149
|
export function renderRenewal(decision: RenewalDecision, grant: Grant): string {
|
|
145
150
|
if (decision.renew)
|
|
146
151
|
return `renewal ${decision.segment - 1} of ${grant.renewals}, continues ${decision.from !== undefined ? decision.from.slice(0, 7) : "the branch's head"}`;
|
|
@@ -151,7 +156,7 @@ export function renderRenewal(decision: RenewalDecision, grant: Grant): string {
|
|
|
151
156
|
switch (decision.why) {
|
|
152
157
|
case "no_progress":
|
|
153
158
|
return decision.renewalsLeft > 0
|
|
154
|
-
? `no progress in the last lease; ${holds};
|
|
159
|
+
? `no progress in the last lease; ${holds} unspent — a renewal is spent only by a segment that pushed to the unit's branch or moved its handoff; re-issue the request to try again`
|
|
155
160
|
: `no progress in the last lease; ${holds}`;
|
|
156
161
|
case "cost_cap":
|
|
157
162
|
return decision.renewalsLeft > 0 ? `${decision.detail}; ${holds} unspent` : decision.detail;
|
|
@@ -17,6 +17,10 @@ export interface AttrDomain {
|
|
|
17
17
|
status: "completed" | "failed" | "refused" | "stopped";
|
|
18
18
|
queuedBeforeMs: number;
|
|
19
19
|
queuedBehindMs: number;
|
|
20
|
+
/** The run this request restarts (a restart from its request, run-history
|
|
21
|
+
* item 54): the page names the predecessor by id instead of inventing a
|
|
22
|
+
* wait out of its lifetime. On the root at start, like the queued numbers. */
|
|
23
|
+
restartOfRunId: string;
|
|
20
24
|
runId: string;
|
|
21
25
|
// slack.receive
|
|
22
26
|
caughtUp: boolean;
|
|
@@ -60,6 +64,14 @@ export interface AttrDomain {
|
|
|
60
64
|
outputTokens: number;
|
|
61
65
|
cacheReadTokens: number;
|
|
62
66
|
cacheWriteTokens: number;
|
|
67
|
+
/** The meter row (model-proxy.md item 6): who bills the turn (the
|
|
68
|
+
* block), the vendor the card names, the turn's dollars with the layer
|
|
69
|
+
* that priced them, and on a BYOK turn the aggregator's fee (inside `usd`). */
|
|
70
|
+
biller: string;
|
|
71
|
+
vendor: string;
|
|
72
|
+
usd: number;
|
|
73
|
+
feeUsd: number;
|
|
74
|
+
priceSource: "provider" | "operator" | "registry" | "none";
|
|
63
75
|
ttftMs: number;
|
|
64
76
|
thinkingMs: number;
|
|
65
77
|
textMs: number;
|
|
@@ -103,6 +115,8 @@ export interface AttrDomain {
|
|
|
103
115
|
signal: "SIGTERM" | "SIGINT" | "other";
|
|
104
116
|
channels: number;
|
|
105
117
|
missed: number;
|
|
118
|
+
/** Catch-up candidates a receipt (or a fresh verdict) silenced — read, never re-run (slack-channel.md item 7). */
|
|
119
|
+
silenced: number;
|
|
106
120
|
orphans: number;
|
|
107
121
|
skipped: number;
|
|
108
122
|
runs: number;
|
|
@@ -125,6 +139,7 @@ export type SpanAttrs = { readonly [K in SpanAttrKey]?: AttrDomain[K] };
|
|
|
125
139
|
* from a closed table, never free text (no whitespace, no `?`/`&`, at most 64
|
|
126
140
|
* chars). */
|
|
127
141
|
const IDENTIFIER_KEYS: ReadonlySet<SpanAttrKey> = new Set<SpanAttrKey>([
|
|
142
|
+
"restartOfRunId",
|
|
128
143
|
"runId",
|
|
129
144
|
"outcome",
|
|
130
145
|
"refusal",
|
|
@@ -135,6 +150,8 @@ const IDENTIFIER_KEYS: ReadonlySet<SpanAttrKey> = new Set<SpanAttrKey>([
|
|
|
135
150
|
"callId",
|
|
136
151
|
"agent",
|
|
137
152
|
"model",
|
|
153
|
+
"biller",
|
|
154
|
+
"vendor",
|
|
138
155
|
]);
|
|
139
156
|
const IDENTIFIER_RE = /^[A-Za-z0-9_./:@+-]{1,64}$/;
|
|
140
157
|
/** A Workflow instance id: the platform's rule (`^[a-zA-Z0-9_][a-zA-Z0-9-_]*$`, at most 100). */
|
|
@@ -172,6 +189,7 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
|
|
|
172
189
|
status: "string",
|
|
173
190
|
queuedBeforeMs: "number",
|
|
174
191
|
queuedBehindMs: "number",
|
|
192
|
+
restartOfRunId: "string",
|
|
175
193
|
runId: "string",
|
|
176
194
|
caughtUp: "boolean",
|
|
177
195
|
files: "number",
|
|
@@ -195,6 +213,11 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
|
|
|
195
213
|
outputTokens: "number",
|
|
196
214
|
cacheReadTokens: "number",
|
|
197
215
|
cacheWriteTokens: "number",
|
|
216
|
+
biller: "string",
|
|
217
|
+
vendor: "string",
|
|
218
|
+
usd: "number",
|
|
219
|
+
feeUsd: "number",
|
|
220
|
+
priceSource: "string",
|
|
198
221
|
ttftMs: "number",
|
|
199
222
|
thinkingMs: "number",
|
|
200
223
|
textMs: "number",
|
|
@@ -230,6 +253,7 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
|
|
|
230
253
|
signal: "string",
|
|
231
254
|
channels: "number",
|
|
232
255
|
missed: "number",
|
|
256
|
+
silenced: "number",
|
|
233
257
|
orphans: "number",
|
|
234
258
|
skipped: "number",
|
|
235
259
|
runs: "number",
|
|
@@ -230,13 +230,13 @@ export interface UploadTicket {
|
|
|
230
230
|
/** What the core needs from a channel to serve one request. */
|
|
231
231
|
/** What a channel shows for a confirmation (`ChannelIO.offer`): the id its
|
|
232
232
|
* affordance carries back, the full command line to run, the one risk line
|
|
233
|
-
* (empty when the command declares none),
|
|
234
|
-
*
|
|
233
|
+
* (empty when the command declares none), and when the offer expires (ms
|
|
234
|
+
* epoch, the config object's clock). No footer: the button is the affordance,
|
|
235
|
+
* and which scope asked is an operator's fact (`config show` names it). */
|
|
235
236
|
export interface ConfirmationOffer {
|
|
236
237
|
id: string;
|
|
237
238
|
line: string;
|
|
238
239
|
risk: string;
|
|
239
|
-
footer: string;
|
|
240
240
|
expiresAt: number;
|
|
241
241
|
/** Present on a question's offer (record 0054): the refusal's sentence,
|
|
242
242
|
* shown above the line, and the evidence naming the match, shown under it.
|
|
@@ -286,8 +286,8 @@ export interface ChannelIO {
|
|
|
286
286
|
/**
|
|
287
287
|
* Show the confirmation a routed write is offered as
|
|
288
288
|
* (docs/reference/specs/routing-and-config.md item 25, record 0044): the full
|
|
289
|
-
* command line the router bound, its one risk line, the
|
|
290
|
-
*
|
|
289
|
+
* command line the router bound, its one risk line, and the id the
|
|
290
|
+
* channel's affordance carries back to
|
|
291
291
|
* `dispatchClick` — a button whose value is the id, on a channel with
|
|
292
292
|
* components. Optional; a channel without it (the CLI, an HTTP reply, the
|
|
293
293
|
* browser) is answered the pasteable line instead, and nothing below the
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Verbosity: how much of itself the bot says in the conversation
|
|
2
|
+
// (docs/reference/specs/routing-and-config.md item 28). Three levels on the
|
|
3
|
+
// log-level pattern — each level shows everything the ones below it show:
|
|
4
|
+
//
|
|
5
|
+
// quiet (default) only what needs the person: answers, results, verdicts,
|
|
6
|
+
// refusals, questions, and the card's own progress.
|
|
7
|
+
// verbose plus every acknowledgement of what the system is doing
|
|
8
|
+
// for the person — a follow-up folded into a live run,
|
|
9
|
+
// a plan handed to the runner, the workspace the run is
|
|
10
|
+
// on, a budget clipped by a boundary.
|
|
11
|
+
// debug plus what an operator debugging the bot reads — the
|
|
12
|
+
// router's reason, the ledger's word on the run.
|
|
13
|
+
//
|
|
14
|
+
// A config dimension resolved through the same layers as `effort` (item 2):
|
|
15
|
+
// a `verbosity:<level>` directive on the request (sticky in the thread from
|
|
16
|
+
// the user's turns, item 3) > the user's scope > the channel's > `defaults`.
|
|
17
|
+
// Nothing here decides WHAT a message is; each message site names the level
|
|
18
|
+
// it belongs to, and the spec's table is the one list of them.
|
|
19
|
+
|
|
20
|
+
export const VERBOSITY_LEVELS = ["quiet", "verbose", "debug"] as const;
|
|
21
|
+
export type Verbosity = (typeof VERBOSITY_LEVELS)[number];
|
|
22
|
+
|
|
23
|
+
/** The level a request runs at when no layer sets one. */
|
|
24
|
+
export const DEFAULT_VERBOSITY: Verbosity = "quiet";
|
|
25
|
+
|
|
26
|
+
/** The valid levels, for error messages: `quiet, verbose, debug`. */
|
|
27
|
+
export const VERBOSITY_LEVELS_HINT = VERBOSITY_LEVELS.join(", ");
|
|
28
|
+
|
|
29
|
+
export function isVerbosity(value: unknown): value is Verbosity {
|
|
30
|
+
return typeof value === "string" && (VERBOSITY_LEVELS as readonly string[]).includes(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether a message that belongs at `floor` is shown at `level`: a level
|
|
34
|
+
* shows its own messages and every lower level's. */
|
|
35
|
+
export function shows(level: Verbosity, floor: Verbosity): boolean {
|
|
36
|
+
return VERBOSITY_LEVELS.indexOf(level) >= VERBOSITY_LEVELS.indexOf(floor);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The level for a request through the layers, most specific first; the
|
|
40
|
+
* default when no layer sets one. */
|
|
41
|
+
export function resolveVerbosity(layers: {
|
|
42
|
+
request?: Verbosity;
|
|
43
|
+
user?: Verbosity;
|
|
44
|
+
channel?: Verbosity;
|
|
45
|
+
defaults?: Verbosity;
|
|
46
|
+
}): Verbosity {
|
|
47
|
+
return layers.request ?? layers.user ?? layers.channel ?? layers.defaults ?? DEFAULT_VERBOSITY;
|
|
48
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { COLD_START_ALLOWANCE_MS, DRAIN_DEADLINE_MS } from "../core/drain.js";
|
|
1
|
+
import { COLD_START_ALLOWANCE_MS, DRAIN_DEADLINE_MS, heldRunsText, type HeldRun } from "../core/drain.js";
|
|
2
2
|
|
|
3
3
|
// "Deployed" is not "live". `wrangler deploy` uploads a Worker version and
|
|
4
4
|
// starts a container rollout, but the OLD bot container keeps serving while it
|
|
@@ -17,6 +17,8 @@ export interface HealthzBody {
|
|
|
17
17
|
inFlight?: unknown;
|
|
18
18
|
draining?: unknown;
|
|
19
19
|
drainStartedAt?: unknown;
|
|
20
|
+
/** While draining: the registry-active runs holding the drain (`HeldRun[]`). */
|
|
21
|
+
held?: unknown;
|
|
20
22
|
build?: { commit?: unknown; builtAt?: unknown } | unknown;
|
|
21
23
|
/** ISO process start — `deploy restart`'s identity (the image, hence `build.commit`, is unchanged). */
|
|
22
24
|
startedAt?: unknown;
|
|
@@ -53,7 +55,8 @@ export type ReadyDecision<Identity> =
|
|
|
53
55
|
export type LiveDecision = ReadyDecision<{ commit: string }>;
|
|
54
56
|
export type RestartDecision = ReadyDecision<{ startedAt: string }>;
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
/** The `build.commit` a Worker's `/healthz` reports — the commit it serves; undefined when absent. */
|
|
59
|
+
export function servedCommit(body: HealthzBody): string | undefined {
|
|
57
60
|
const b = body.build;
|
|
58
61
|
if (typeof b !== "object" || b === null) return undefined;
|
|
59
62
|
const c = (b as { commit?: unknown }).commit;
|
|
@@ -84,14 +87,35 @@ function decideReady<Identity>(
|
|
|
84
87
|
const verdict = identify(body);
|
|
85
88
|
if (verdict.live) return { kind: "live", ...verdict.identity };
|
|
86
89
|
if (body.draining === true && !verdict.sameIdentity) {
|
|
87
|
-
const n = typeof body.inFlight === "number" ? body.inFlight : "?";
|
|
88
90
|
const since = typeof body.drainStartedAt === "string" ? ` since ${body.drainStartedAt}` : "";
|
|
89
|
-
|
|
91
|
+
// What is actually held, when the body says: the registry-active run ids
|
|
92
|
+
// and why. The dispatcher's `inFlight` can read 0 while a registry row
|
|
93
|
+
// holds the drain for its full deadline — a count that mystifies the
|
|
94
|
+
// operator watching the wait; the ids do not.
|
|
95
|
+
const held = heldRuns(body);
|
|
96
|
+
if (held.length > 0) {
|
|
97
|
+
reason = `old container still draining — holding ${held.length} registry-active run(s): ${heldRunsText(held)}${since}`;
|
|
98
|
+
} else {
|
|
99
|
+
const n = typeof body.inFlight === "number" ? body.inFlight : "?";
|
|
100
|
+
reason = `old container still draining — ${n} run(s) in flight${since}`;
|
|
101
|
+
}
|
|
90
102
|
} else reason = verdict.reason;
|
|
91
103
|
}
|
|
92
104
|
return elapsedMs >= deadlineMs ? { kind: "timeout", reason } : { kind: "waiting", reason };
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
/** The `held` rows of a `/healthz` body (src/channels/health.ts): the
|
|
108
|
+
* registry-active runs holding the drain. Rows that are not `{id, why}`
|
|
109
|
+
* strings — an older container's body, or no drain — parse to none. */
|
|
110
|
+
export function heldRuns(body: HealthzBody): HeldRun[] {
|
|
111
|
+
if (!Array.isArray(body.held)) return [];
|
|
112
|
+
return body.held.flatMap((r: unknown) => {
|
|
113
|
+
if (typeof r !== "object" || r === null) return [];
|
|
114
|
+
const { id, why } = r as { id?: unknown; why?: unknown };
|
|
115
|
+
return typeof id === "string" && typeof why === "string" ? [{ id, why }] : [];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
95
119
|
/** Two commit identities name the same commit when one is a prefix of the other
|
|
96
120
|
* (≥7 chars) — `git rev-parse HEAD` vs. a short form; a `-dirty` suffix never matches. */
|
|
97
121
|
export function sameCommit(a: string, b: string): boolean {
|
|
@@ -195,25 +219,28 @@ export interface GaveUpWords {
|
|
|
195
219
|
force: string;
|
|
196
220
|
}
|
|
197
221
|
|
|
198
|
-
/** `deploy all`'s words: the release job is re-run once
|
|
222
|
+
/** `deploy all`'s words: the release job is re-run once what refuses clears.
|
|
223
|
+
* In-flight bot runs never refuse (they hand off — run-history item 39); what
|
|
224
|
+
* a bot refusal names is a rollout still settling, and a resident refusal is
|
|
225
|
+
* its runs in flight — which `--force` over the resident would kill. */
|
|
199
226
|
export const DEPLOY_GAVE_UP_WORDS: GaveUpWords = {
|
|
200
227
|
notDone: "NOT deployed",
|
|
201
|
-
rerun: "re-run the deploy once
|
|
202
|
-
force: "--force to deploy over
|
|
228
|
+
rerun: "re-run the deploy once it clears (a CI job: `gh run rerun RUN_ID --failed`)",
|
|
229
|
+
force: "--force to deploy over it (kills any resident runs in flight that no resume recovers)",
|
|
203
230
|
};
|
|
204
231
|
|
|
205
|
-
/** `deploy restart`'s words.
|
|
232
|
+
/** `deploy restart`'s words. Its refusals are the fail-closed cases only —
|
|
233
|
+
* runs in flight hand off and never refuse. */
|
|
206
234
|
export const RESTART_GAVE_UP_WORDS: GaveUpWords = {
|
|
207
235
|
notDone: "NOT restarted",
|
|
208
|
-
rerun: "re-run `deploy restart` once
|
|
209
|
-
force: "--force to stop
|
|
236
|
+
rerun: "re-run `deploy restart` once the bot answers",
|
|
237
|
+
force: "--force to stop blind",
|
|
210
238
|
};
|
|
211
239
|
|
|
212
240
|
/** The failure a preflight still refusing at the end of the wait budget
|
|
213
241
|
* produces. The wait is only how long to hold before failing: it never ends
|
|
214
|
-
* in a deploy over what refused
|
|
215
|
-
*
|
|
216
|
-
* the refusal, that nothing was done, and the two ways forward. */
|
|
242
|
+
* in a deploy over what refused, so the line names the budget, the refusal,
|
|
243
|
+
* that nothing was done, and the two ways forward. */
|
|
217
244
|
export function preflightGaveUpLine(
|
|
218
245
|
waitMaxMs: number,
|
|
219
246
|
reason: string,
|
|
@@ -100,6 +100,22 @@ export const profileSchema = z.object({
|
|
|
100
100
|
.regex(/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/, "an R2 bucket name: 3–63 lowercase letters, digits and hyphens"),
|
|
101
101
|
})
|
|
102
102
|
.optional(),
|
|
103
|
+
/** The run-metrics dataset (docs/reference/specs/run-metrics.md item 6), when the installation
|
|
104
|
+
* writes points: the state Worker's template binds it as `RUN_METRICS` with the name beside it
|
|
105
|
+
* in `RUN_METRICS_DATASET`, and `deploy plan` names the binding on the memory step. Nothing is
|
|
106
|
+
* created ahead of the deploy — the platform creates a dataset on first write. The bot's runtime
|
|
107
|
+
* config (`metrics.dataset`) must name the same dataset; the two are held together by the bot's
|
|
108
|
+
* boot warning, not by the profile. Analytics Engine's own dataset-name rules. */
|
|
109
|
+
metrics: z
|
|
110
|
+
.object({
|
|
111
|
+
dataset: z
|
|
112
|
+
.string()
|
|
113
|
+
.regex(
|
|
114
|
+
/^[A-Za-z_][A-Za-z0-9_]{0,63}$/,
|
|
115
|
+
"an Analytics Engine dataset name: 1–64 letters, digits and underscores, not starting with a digit",
|
|
116
|
+
),
|
|
117
|
+
})
|
|
118
|
+
.optional(),
|
|
103
119
|
});
|
|
104
120
|
|
|
105
121
|
export type DeploymentProfile = z.infer<typeof profileSchema>;
|
|
@@ -53,21 +53,20 @@ export const RESTART_SCOPE = "deploy:write";
|
|
|
53
53
|
export interface RestartVerdict {
|
|
54
54
|
allow: boolean;
|
|
55
55
|
forced: boolean;
|
|
56
|
-
/** What refuses
|
|
56
|
+
/** What refuses (fail-closed: no JSON body, an impossible count). */
|
|
57
57
|
problems: string[];
|
|
58
|
-
/** What is said but does not refuse: a drain under way
|
|
58
|
+
/** What is said but does not refuse: runs in flight (they hand off), a drain under way. */
|
|
59
59
|
warnings: string[];
|
|
60
60
|
message: string;
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Whether the container may be stopped now — the deploy preflight's rules
|
|
64
64
|
* (deploy/cloudflare/preflight.mjs `decide`) minus the rollout-state check (a
|
|
65
|
-
* restart is not a rollout).
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* warning.
|
|
70
|
-
* `force` allows everything anyway, flagged, naming what it kills.
|
|
65
|
+
* restart is not a rollout). Since the handoff (docs/reference/specs/run-history.md item
|
|
66
|
+
* 39) runs in flight and a drain under way are WARNINGS, not refusals: SIGTERM
|
|
67
|
+
* hands every resumable run to the next generation. Fail closed on a body
|
|
68
|
+
* that is not JSON or an impossible count; `force` allows those anyway, with
|
|
69
|
+
* the warning.
|
|
71
70
|
*/
|
|
72
71
|
export function decideRestart(body: HealthzBody | undefined, opts: { force: boolean }): RestartVerdict {
|
|
73
72
|
const problems: string[] = [];
|
|
@@ -80,8 +79,8 @@ export function decideRestart(body: HealthzBody | undefined, opts: { force: bool
|
|
|
80
79
|
if (!Number.isInteger(body.inFlight) || (body.inFlight as number) < 0) {
|
|
81
80
|
problems.push(`bot reports an impossible inFlight=${JSON.stringify(body.inFlight)} (counter bug or old Worker)`);
|
|
82
81
|
} else if ((body.inFlight as number) > 0) {
|
|
83
|
-
|
|
84
|
-
`${body.inFlight} run(s) in flight —
|
|
82
|
+
warnings.push(
|
|
83
|
+
`${body.inFlight} run(s) in flight — handed to the next generation on SIGTERM (run-history item 39); they continue there`,
|
|
85
84
|
);
|
|
86
85
|
}
|
|
87
86
|
if (body.draining === true)
|
|
@@ -99,14 +98,14 @@ export function decideRestart(body: HealthzBody | undefined, opts: { force: bool
|
|
|
99
98
|
forced: true,
|
|
100
99
|
problems,
|
|
101
100
|
warnings,
|
|
102
|
-
message: `restart WARNING: stopping by force despite —\n${detail}
|
|
101
|
+
message: `restart WARNING: stopping by force despite —\n${detail}`,
|
|
103
102
|
};
|
|
104
103
|
return {
|
|
105
104
|
allow: false,
|
|
106
105
|
forced: false,
|
|
107
106
|
problems,
|
|
108
107
|
warnings,
|
|
109
|
-
message: `restart REFUSED —\n${detail}\n wait
|
|
108
|
+
message: `restart REFUSED —\n${detail}\n wait and retry, or pass --force to stop blind`,
|
|
110
109
|
};
|
|
111
110
|
}
|
|
112
111
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* fully owned by the thread user, overwritable, and still isolated from the
|
|
18
18
|
* warm checkout (a copy shares nothing). */
|
|
19
19
|
|
|
20
|
+
import { nestedNodeModulesListCmd } from "./residentDepsStore.js";
|
|
20
21
|
import { shellQuote } from "./shellQuote.js";
|
|
21
22
|
|
|
22
23
|
export const DEP_CACHE_DIRS = ["node_modules", "dist", "build", "out", ".next"] as const;
|
|
@@ -217,6 +218,10 @@ export interface DepCacheScriptParse {
|
|
|
217
218
|
* node_modules) it left in place because the source has no counterpart —
|
|
218
219
|
* tree-private entries, not shared inodes (see `mutableCacheSwapScript`). */
|
|
219
220
|
skipped: string[];
|
|
221
|
+
/** Tree-relative nested node_modules dirs the script hardlinked from the
|
|
222
|
+
* store entry (`deploy/w/node_modules`): each needs its own tool-cache
|
|
223
|
+
* swap, scoped to that root. */
|
|
224
|
+
nested: string[];
|
|
220
225
|
failedStep: string | null;
|
|
221
226
|
}
|
|
222
227
|
|
|
@@ -273,6 +278,47 @@ export function depCacheScript(
|
|
|
273
278
|
`fi`,
|
|
274
279
|
].join("\n");
|
|
275
280
|
});
|
|
281
|
+
// A store-backed view also materializes every NESTED node_modules the entry
|
|
282
|
+
// carries (item 59: the entry holds everything the install produced under a
|
|
283
|
+
// node_modules — npm installs a workspace's conflicting versions into the
|
|
284
|
+
// workspace's own node_modules, and a tree without them drifts from its
|
|
285
|
+
// lockfile: hoisted packages lose their dependent and `npm ls --omit=dev`
|
|
286
|
+
// misattributes their subtrees to production). Same mechanism per dir as the
|
|
287
|
+
// top-level one — cp -al, the combined chown/harden walk, the mutable-cache
|
|
288
|
+
// listing — gated on the tree having the parent dir (a worktree at another
|
|
289
|
+
// sha may not) and nothing already there. Each hardlinked dir is named on a
|
|
290
|
+
// `nested=<tree-relative path>` line so the Worker swaps its tool caches.
|
|
291
|
+
const entryRoot = opts.nodeModulesSrc?.endsWith("/node_modules")
|
|
292
|
+
? opts.nodeModulesSrc.slice(0, -"/node_modules".length)
|
|
293
|
+
: undefined;
|
|
294
|
+
if (entryRoot) {
|
|
295
|
+
const entryQ = shellQuote(entryRoot);
|
|
296
|
+
const wtQ = shellQuote(worktree);
|
|
297
|
+
const nestedWalk = `find "$dst" \\( -type d -exec chown ${owner} {} + \\) -o \\( -type f \\( -perm -g+w -o -perm -o+w \\) -exec chmod go-w {} + \\)`;
|
|
298
|
+
blocks.push(
|
|
299
|
+
[
|
|
300
|
+
`if test -d ${entryQ}; then`,
|
|
301
|
+
` ${nestedNodeModulesListCmd(entryRoot)} | while IFS= read -r rel; do`,
|
|
302
|
+
` src=${entryQ}/"$rel"`,
|
|
303
|
+
` dst=${wtQ}/"$rel"`,
|
|
304
|
+
` pdir="\${dst%/node_modules}"`,
|
|
305
|
+
` if test -d "$pdir" && ! test -e "$dst"; then`,
|
|
306
|
+
` if cp -al "$src" "$dst"; then`,
|
|
307
|
+
` ${nestedWalk} || { echo err=deps-perms; exit 1; }`,
|
|
308
|
+
` if ! mlist=$(find "$dst" -mindepth 1 \\( -path "$dst/.*" -o -type d -name .cache \\)); then echo err=deps-mutable-list; exit 1; fi`,
|
|
309
|
+
` if [ -n "$mlist" ]; then printf '%s\\n' "$mlist" | sed 's/^/mutable=/'; fi`,
|
|
310
|
+
` echo "nested=$rel"`,
|
|
311
|
+
` else`,
|
|
312
|
+
` rm -rf "$dst"`,
|
|
313
|
+
` cp -R "$src" "$dst" || { echo err=deps-copy; exit 1; }`,
|
|
314
|
+
` chown -Rh ${owner} "$dst" || { echo err=deps-copy-chown; exit 1; }`,
|
|
315
|
+
` fi`,
|
|
316
|
+
` fi`,
|
|
317
|
+
` done || exit 1`,
|
|
318
|
+
`fi`,
|
|
319
|
+
].join("\n"),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
276
322
|
return blocks.join("\n");
|
|
277
323
|
}
|
|
278
324
|
|
|
@@ -280,6 +326,7 @@ export function parseDepCacheScriptOutput(stdout: string): DepCacheScriptParse {
|
|
|
280
326
|
let mech: DepCacheMaterialization | "none" = "none";
|
|
281
327
|
const mutableListing: string[] = [];
|
|
282
328
|
const skipped: string[] = [];
|
|
329
|
+
const nested: string[] = [];
|
|
283
330
|
let failedStep: string | null = null;
|
|
284
331
|
for (const raw of stdout.split("\n")) {
|
|
285
332
|
const line = raw.trim();
|
|
@@ -292,11 +339,13 @@ export function parseDepCacheScriptOutput(stdout: string): DepCacheScriptParse {
|
|
|
292
339
|
mutableListing.push(m[1]);
|
|
293
340
|
} else if ((m = /^skipped=(.+)$/.exec(line))) {
|
|
294
341
|
skipped.push(m[1]);
|
|
342
|
+
} else if ((m = /^nested=(.+)$/.exec(line))) {
|
|
343
|
+
nested.push(m[1]);
|
|
295
344
|
} else if ((m = /^err=(.+)$/.exec(line))) {
|
|
296
345
|
failedStep ??= m[1];
|
|
297
346
|
}
|
|
298
347
|
}
|
|
299
|
-
return { mech, mutableListing, skipped, failedStep };
|
|
348
|
+
return { mech, mutableListing, skipped, nested, failedStep };
|
|
300
349
|
}
|
|
301
350
|
|
|
302
351
|
/** The per-path swaps for a hardlinked node_modules' tool-managed entries
|
|
@@ -115,11 +115,30 @@ export function planDepsMaterialization(input: {
|
|
|
115
115
|
/** The checkout snapshot leaves out its top-level node_modules: since item 59
|
|
116
116
|
* that directory is a hardlink view of an immutable store entry, and the
|
|
117
117
|
* entry has its own backup (below). Nested node_modules (a workspace package's
|
|
118
|
-
* own) stay in — small, and
|
|
118
|
+
* own) stay in — small, and adoption of an old snapshot re-reads them. The
|
|
119
119
|
* pattern is anchored at the archive root (mksquashfs wildcard semantics:
|
|
120
120
|
* a bare name matches only there; `...`-prefixed patterns match anywhere). */
|
|
121
121
|
export const CHECKOUT_SNAPSHOT_EXCLUDES: readonly string[] = ["node_modules"];
|
|
122
122
|
|
|
123
|
+
/** The entry archive is the entry dir itself minus its markers: `.complete`
|
|
124
|
+
* must be written by the restore's own commit, LAST, never extracted — a
|
|
125
|
+
* partial download that carried the marker would read as a complete entry. */
|
|
126
|
+
export const DEPS_ENTRY_BACKUP_EXCLUDES: readonly string[] = [".complete", ".used"];
|
|
127
|
+
|
|
128
|
+
/** List every OUTERMOST node_modules under `rootDir` EXCEPT the top-level
|
|
129
|
+
* one, tree-relative (`deploy/w/node_modules`), one per line. npm installs a
|
|
130
|
+
* workspace's conflicting versions into the workspace's own node_modules
|
|
131
|
+
* (the lockfile names those paths), so an entry or a view that carries only
|
|
132
|
+
* the top-level dir leaves the tree short of what its lockfile mandates —
|
|
133
|
+
* packages hoisted for a nested dependent then read as extraneous, and
|
|
134
|
+
* `npm ls --omit=dev` misattributes their subtrees to production (the
|
|
135
|
+
* licenses:check LGPL failure that named this). `-prune` keeps copies
|
|
136
|
+
* nested INSIDE a listed node_modules with their parent; `.git` is never a
|
|
137
|
+
* source of entries. */
|
|
138
|
+
export function nestedNodeModulesListCmd(rootDir: string): string {
|
|
139
|
+
return `(cd ${shellQuote(rootDir)} && find . \\( -path ./node_modules -o -name .git \\) -prune -o -name node_modules -type d -prune -print | sed 's|^\\./||')`;
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
/** One backup per lockfile key, taken ONCE right after the entry is committed
|
|
124
143
|
* (install or adoption) and never again: the entry is immutable, so its
|
|
125
144
|
* archive is too. Recorded on the DO under this prefix, by key. */
|
|
@@ -194,10 +213,20 @@ export function depsHardenScript(input: {
|
|
|
194
213
|
emptyOk: boolean;
|
|
195
214
|
}): string {
|
|
196
215
|
const nm = shellQuote(`${input.scratchDir}/node_modules`);
|
|
216
|
+
const scratch = shellQuote(input.scratchDir);
|
|
197
217
|
const absent = input.emptyOk
|
|
198
218
|
? `mkdir ${nm} && chown ${input.owner} ${nm}`
|
|
199
219
|
: `echo "install produced no node_modules in ${input.scratchDir}" >&2; exit 1`;
|
|
200
|
-
return [
|
|
220
|
+
return [
|
|
221
|
+
`set -e`,
|
|
222
|
+
`test -d ${nm} || { ${absent}; }`,
|
|
223
|
+
`find ${nm} -type f -perm -u+w -exec chmod u-w {} +`,
|
|
224
|
+
// Nested workspace node_modules become entry content too (the commit
|
|
225
|
+
// script moves them), so their inodes are hardened the same way.
|
|
226
|
+
`${nestedNodeModulesListCmd(input.scratchDir)} | while IFS= read -r rel; do`,
|
|
227
|
+
` find ${scratch}/"$rel" -type f -perm -u+w -exec chmod u-w {} + || exit 1`,
|
|
228
|
+
`done`,
|
|
229
|
+
].join("\n");
|
|
201
230
|
}
|
|
202
231
|
|
|
203
232
|
/** Commit an install to the store, as root, in the order that makes the entry
|
|
@@ -221,6 +250,7 @@ export function depsStoreCommitScript(input: {
|
|
|
221
250
|
keepScratch?: boolean;
|
|
222
251
|
}): string {
|
|
223
252
|
const scratchNm = shellQuote(`${input.scratchDir}/node_modules`);
|
|
253
|
+
const scratchDirQ = shellQuote(input.scratchDir);
|
|
224
254
|
const staging = shellQuote(input.stagingDir);
|
|
225
255
|
const stagingNm = shellQuote(`${input.stagingDir}/node_modules`);
|
|
226
256
|
const entry = shellQuote(input.entryDir);
|
|
@@ -232,6 +262,14 @@ export function depsStoreCommitScript(input: {
|
|
|
232
262
|
`rm -rf ${staging}`,
|
|
233
263
|
`mkdir ${staging}`,
|
|
234
264
|
`mv ${scratchNm} ${stagingNm}`,
|
|
265
|
+
// Every outermost NESTED node_modules moves too, at its tree-relative
|
|
266
|
+
// path (the top-level one is already in staging, so the walk never sees
|
|
267
|
+
// it): the entry must hold everything the install produced under a
|
|
268
|
+
// node_modules, or every view of it drifts from its lockfile.
|
|
269
|
+
`${nestedNodeModulesListCmd(input.scratchDir)} | while IFS= read -r rel; do`,
|
|
270
|
+
` mkdir -p ${staging}/"$(dirname "$rel")" || exit 1`,
|
|
271
|
+
` mv ${scratchDirQ}/"$rel" ${staging}/"$rel" || exit 1`,
|
|
272
|
+
`done`,
|
|
235
273
|
// An entry dir WITHOUT its marker is crash debris (the shell died between
|
|
236
274
|
// the rename and the touch): remove it, or the rename below would nest
|
|
237
275
|
// the new staging inside it and the touch would mark the pair complete.
|
|
@@ -93,15 +93,67 @@ export function planRefresh(input: {
|
|
|
93
93
|
* through shared inodes (review 1b). Pruned at any depth (workspaces). */
|
|
94
94
|
export const NODE_MODULES_CACHE_DIRS = [".cache", ".vite"] as const;
|
|
95
95
|
|
|
96
|
-
/** The
|
|
97
|
-
*
|
|
96
|
+
/** The one command of the rebuild that READS THE MIRROR — run as the build
|
|
97
|
+
* user in the staging tree, inside the stage lock (`stageCheckoutScript`'s
|
|
98
|
+
* lease): an attach's `fetch --prune` into the mirror could otherwise delete
|
|
99
|
+
* a ref out from under this fetch's negotiation. Everything after it (reset,
|
|
100
|
+
* clean, build) touches only the staging tree and runs outside the lock. */
|
|
101
|
+
export const CHECKOUT_FETCH_COMMAND = "git fetch --quiet origin";
|
|
102
|
+
|
|
103
|
+
/** The directories of the staged rebuild (resident-repos.md item 47): the
|
|
104
|
+
* warm checkout, the staging tree the rebuild happens in, and the retired
|
|
105
|
+
* path the replaced checkout waits at between the swap's rename and its
|
|
106
|
+
* removal. All on one filesystem, so every move below is a rename. */
|
|
107
|
+
export interface CheckoutSwapDirs {
|
|
108
|
+
checkout: string;
|
|
109
|
+
staging: string;
|
|
110
|
+
retired: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Root shell that seeds the staging tree, under the mirror lock — the one
|
|
114
|
+
* hardlink copy of the checkout the lock still covers, exactly
|
|
115
|
+
* materializeThreadDeps' consistency guarantee. First the self-heal: a
|
|
116
|
+
* container killed between the swap's two renames leaves no checkout and a
|
|
117
|
+
* complete retired tree, which is moved back before anything else reads the
|
|
118
|
+
* checkout path. Then a previous attempt's leftovers go — the caller already
|
|
119
|
+
* dropped the staging tree OFF the lock (a failed build leaves hundreds of
|
|
120
|
+
* thousands of inodes; the tree is private, so its removal needs no lock),
|
|
121
|
+
* leaving this `rm` a near-instant residual guard; the retired tree's
|
|
122
|
+
* heal-then-remove ordering is what needs the lock. Last, `cp -al` takes
|
|
123
|
+
* the copy (hardlinks — seconds, never a byte copy; run by root, ownership
|
|
124
|
+
* preserved). */
|
|
125
|
+
export function stageCheckoutScript(dirs: CheckoutSwapDirs): string {
|
|
126
|
+
const { checkout, staging, retired } = dirs;
|
|
127
|
+
return (
|
|
128
|
+
`if [ ! -d ${checkout}/.git ] && [ -d ${retired}/.git ]; then mv ${retired} ${checkout}; fi && ` +
|
|
129
|
+
`rm -rf ${staging} ${retired} && cp -al ${checkout} ${staging}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Root shell that swaps the built staging tree into the checkout path, under
|
|
134
|
+
* the mirror lock: two renames — milliseconds, so an attach's 60 s wait is
|
|
135
|
+
* never spent on a build. If the second rename fails the retired tree is put
|
|
136
|
+
* back (the checkout path is never left empty for a copier) and the step
|
|
137
|
+
* fails; the retired tree itself is removed by the caller AFTER the lock is
|
|
138
|
+
* released, so the lock is never held for a large `rm`. */
|
|
139
|
+
export function swapCheckoutScript(dirs: CheckoutSwapDirs): string {
|
|
140
|
+
const { checkout, staging, retired } = dirs;
|
|
141
|
+
return (
|
|
142
|
+
`mv ${checkout} ${retired} && mv ${staging} ${checkout} || ` +
|
|
143
|
+
`{ if [ ! -d ${checkout} ] && [ -d ${retired}/.git ]; then mv ${retired} ${checkout}; fi; exit 1; }`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The checkout-update shell for the build user (runs inside the staging
|
|
148
|
+
* tree, OUTSIDE the mirror lock — the fetch is `CHECKOUT_FETCH_COMMAND`,
|
|
149
|
+
* under the stage lock): hard-reset to `sha`, then the `-x` clean.
|
|
98
150
|
* `-e node_modules` is a git exclude pattern (matches at any depth, so
|
|
99
151
|
* workspace packages keep theirs too) that survives `-x`; everything else
|
|
100
152
|
* gitignored — build output above all — is still removed so the build
|
|
101
153
|
* allocates fresh inodes (review 1b). Keep-deps additionally sweeps the
|
|
102
154
|
* build-written caches inside node_modules (NODE_MODULES_CACHE_DIRS). */
|
|
103
155
|
export function checkoutUpdateCommand(sha: string, clean: CleanScope): string {
|
|
104
|
-
const base = `git
|
|
156
|
+
const base = `git reset --hard --quiet ${sha}`;
|
|
105
157
|
if (clean === "all") return `${base} && git clean -fdx`;
|
|
106
158
|
const names = NODE_MODULES_CACHE_DIRS.map((d) => `-name ${d}`).join(" -o ");
|
|
107
159
|
const sweep = `find . -path '*/node_modules/*' -type d \\( ${names} \\) -prune -exec rm -rf {} +`;
|
|
@@ -18,6 +18,10 @@ export const RESIDENT_STEP_LABELS = {
|
|
|
18
18
|
"for-each-ref": "listing the branches",
|
|
19
19
|
checkout: "checking out the branch",
|
|
20
20
|
"checkout-update": "updating the checkout",
|
|
21
|
+
"checkout-stage": "staging the checkout rebuild",
|
|
22
|
+
"checkout-stage-clear": "removing the leftover staging tree",
|
|
23
|
+
"checkout-swap": "swapping in the rebuilt checkout",
|
|
24
|
+
"checkout-retire": "removing the replaced checkout",
|
|
21
25
|
"rev-parse": "reading the commit",
|
|
22
26
|
"cat-file": "checking the mirror for the commit",
|
|
23
27
|
"show-ref": "reading the branch tip",
|
|
@@ -344,6 +344,14 @@ export function runtimeUnreachableExecAnswer(message: string): {
|
|
|
344
344
|
/** The named reason the Worker answers with, beside `fleet-busy` and `sandbox-starting`. */
|
|
345
345
|
export const RUNTIME_BUSY_REASON = "runtime-busy" as const;
|
|
346
346
|
|
|
347
|
+
/** The resident's own word for a container near its cgroup memory cap
|
|
348
|
+
* (resident-repos.md item 70): a new attach above the soft threshold and a
|
|
349
|
+
* new exec above the hard one are refused with this token — the same 503
|
|
350
|
+
* shape as `mirror-busy`, so the bot falls back or waits legibly while the
|
|
351
|
+
* commands already running finish. Defined here, beside the other machine
|
|
352
|
+
* tokens both sides read, so the Worker and the bot cannot drift. */
|
|
353
|
+
export const MEMORY_PRESSURE_REASON = "memory-pressure" as const;
|
|
354
|
+
|
|
347
355
|
/** What the token means, in the words the model and the operator see. */
|
|
348
356
|
export const RUNTIME_BUSY_EXPLANATION =
|
|
349
357
|
"the thread's sandbox container is running but did not accept the connection inside the platform's allowance — nothing ran, the request is re-sent once it accepts";
|