@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
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
type UnitPipelineInput,
|
|
58
58
|
type UnitPipelineState,
|
|
59
59
|
} from "../ship/coordinator.js";
|
|
60
|
-
import { isCoordinatorUnit, runFinishedEventType, type CoordinatorUnit } from "./contract.js";
|
|
60
|
+
import { checksSettledEventType, isCoordinatorUnit, runFinishedEventType, type CoordinatorUnit } from "./contract.js";
|
|
61
61
|
|
|
62
62
|
const MIN = 60_000;
|
|
63
63
|
|
|
@@ -429,6 +429,22 @@ async function perform(
|
|
|
429
429
|
case "sleep":
|
|
430
430
|
await step.sleep(action.step, action.ms);
|
|
431
431
|
return { type: "sleep", step: action.step };
|
|
432
|
+
case "wait-checks": {
|
|
433
|
+
// The intake's checks-settled event at the approved head (http-ingress.md
|
|
434
|
+
// item 12), with the machine's bounded timeout as the fallback: either
|
|
435
|
+
// way the machine re-asks the merge door, which is the guard.
|
|
436
|
+
let outcome: "event" | "timeout";
|
|
437
|
+
try {
|
|
438
|
+
await step.waitForEvent(action.step, {
|
|
439
|
+
type: checksSettledEventType(action.headSha),
|
|
440
|
+
timeout: action.timeoutMs,
|
|
441
|
+
});
|
|
442
|
+
outcome = "event";
|
|
443
|
+
} catch {
|
|
444
|
+
outcome = "timeout";
|
|
445
|
+
}
|
|
446
|
+
return { type: "wait-checks", step: action.step, outcome };
|
|
447
|
+
}
|
|
432
448
|
case "merge":
|
|
433
449
|
return mergeReturn(
|
|
434
450
|
action.step,
|
|
@@ -131,6 +131,78 @@ export function parseInstanceStatusPath(pathname: string): string | undefined {
|
|
|
131
131
|
return INSTANCE_ID_PATTERN.test(id) ? id : undefined;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** `POST /admin/coordinator/instances/<id>/events` — the shim's event relay
|
|
135
|
+
* (docs/reference/specs/http-ingress.md item 12): the bot holds no Workflow
|
|
136
|
+
* binding, so the check-run intake sends `checks-settled-<head>` through this
|
|
137
|
+
* route and the shim's own `SHIP_COORDINATOR.get(id).sendEvent` delivers it. */
|
|
138
|
+
export const COORDINATOR_INSTANCE_EVENTS_SUFFIX = "/events";
|
|
139
|
+
|
|
140
|
+
/** The instance id an event path names, or undefined for any other path. */
|
|
141
|
+
export function parseInstanceEventPath(pathname: string): string | undefined {
|
|
142
|
+
if (!pathname.startsWith(COORDINATOR_INSTANCE_STATUS_PREFIX)) return undefined;
|
|
143
|
+
if (!pathname.endsWith(COORDINATOR_INSTANCE_EVENTS_SUFFIX)) return undefined;
|
|
144
|
+
const id = pathname.slice(
|
|
145
|
+
COORDINATOR_INSTANCE_STATUS_PREFIX.length,
|
|
146
|
+
pathname.length - COORDINATOR_INSTANCE_EVENTS_SUFFIX.length,
|
|
147
|
+
);
|
|
148
|
+
return INSTANCE_ID_PATTERN.test(id) ? id : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type ParsedSendEvent = { ok: true; type: string; payload: unknown } | { ok: false; reason: string };
|
|
152
|
+
|
|
153
|
+
/** The Workflow event alphabet — what the engine's `sendEvent` accepts as a type. */
|
|
154
|
+
const EVENT_TYPE_PATTERN = /^[A-Za-z0-9_-]{1,100}$/;
|
|
155
|
+
|
|
156
|
+
/** The body: `{ type, payload? }` — the typed event the instance's `waitForEvent` waits on. */
|
|
157
|
+
export function parseSendEventRequest(text: string): ParsedSendEvent {
|
|
158
|
+
let parsed: unknown;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(text);
|
|
161
|
+
} catch {
|
|
162
|
+
return { ok: false, reason: "body is not valid JSON" };
|
|
163
|
+
}
|
|
164
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
165
|
+
return { ok: false, reason: "body must be a JSON object" };
|
|
166
|
+
const b = parsed as Record<string, unknown>;
|
|
167
|
+
if (typeof b.type !== "string" || !EVENT_TYPE_PATTERN.test(b.type))
|
|
168
|
+
return { ok: false, reason: "`type` must be a Workflow event type: letters, digits, `_` and `-`, at most 100" };
|
|
169
|
+
return { ok: true, type: b.type, payload: b.payload };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** How the send ended on the shim: delivered, no such instance (the engine's
|
|
173
|
+
* own `instance.not_found`), or the engine refusing by reason (an instance
|
|
174
|
+
* that already ended is a refusal, not an absence). */
|
|
175
|
+
export type SendEventOutcome =
|
|
176
|
+
{ kind: "sent"; id: string } | { kind: "absent"; id: string } | { kind: "failed"; id: string; reason: string };
|
|
177
|
+
|
|
178
|
+
export function sendEventResponse(outcome: SendEventOutcome): { status: number; body: Record<string, unknown> } {
|
|
179
|
+
switch (outcome.kind) {
|
|
180
|
+
case "sent":
|
|
181
|
+
return { status: 200, body: { ok: true, id: outcome.id, sent: true } };
|
|
182
|
+
case "absent":
|
|
183
|
+
return { status: 404, body: { ok: false, error: "no_instance", id: outcome.id } };
|
|
184
|
+
case "failed":
|
|
185
|
+
return { status: 502, body: { ok: false, error: "send_failed", id: outcome.id, message: outcome.reason } };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The send answer as the bot reads it back: sent, absent, or unanswered by reason. */
|
|
190
|
+
export type SendEventAnswer = { kind: "sent" } | { kind: "absent" } | { kind: "unanswered"; reason: string };
|
|
191
|
+
|
|
192
|
+
export function readSendEventAnswer(status: number, text: string): SendEventAnswer {
|
|
193
|
+
let parsed: unknown;
|
|
194
|
+
try {
|
|
195
|
+
parsed = JSON.parse(text);
|
|
196
|
+
} catch {
|
|
197
|
+
parsed = undefined;
|
|
198
|
+
}
|
|
199
|
+
const body = typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>) : undefined;
|
|
200
|
+
if (status === 200 && body?.ok === true && body.sent === true) return { kind: "sent" };
|
|
201
|
+
if (status === 404 && body?.error === "no_instance") return { kind: "absent" };
|
|
202
|
+
const detail = typeof body?.error === "string" ? body.error : text.slice(0, 200);
|
|
203
|
+
return { kind: "unanswered", reason: `HTTP ${status} — ${detail}` };
|
|
204
|
+
}
|
|
205
|
+
|
|
134
206
|
/** How the status read ended on the shim: the platform's status word, no such
|
|
135
207
|
* instance, or the engine failing by reason. */
|
|
136
208
|
export type InstanceStatusOutcome =
|
|
@@ -72,6 +72,9 @@ export interface CostsConfig {
|
|
|
72
72
|
snapshot: {
|
|
73
73
|
/** Hours between two reads of the billing sources. */
|
|
74
74
|
everyHours: number;
|
|
75
|
+
/** The platform-namespaced channel (`slack:C…`) told when takes keep failing and when they
|
|
76
|
+
* land again (`ALERT_AFTER_FAILURES` in a row); absent → the status alone says so. */
|
|
77
|
+
alertChannel?: string;
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -82,13 +85,15 @@ const DEFAULT_ANTHROPIC_ADMIN_ENV = "ANTHROPIC_ADMIN_KEY";
|
|
|
82
85
|
* default — both sources bucket by UTC day, and the page is read about as often. */
|
|
83
86
|
export const COSTS_SNAPSHOT_EVERY_HOURS = Object.freeze({ default: 24, min: 1, max: 168 });
|
|
84
87
|
|
|
85
|
-
/** `costs.snapshot`: absent → the default interval; a value outside the
|
|
88
|
+
/** `costs.snapshot`: absent → the default interval and no alert channel; a value outside the
|
|
89
|
+
* bounds or not a whole number throws by name; `alertChannel`, when given, is a platform-namespaced id. */
|
|
86
90
|
function snapshotConfig(raw: unknown): CostsConfig["snapshot"] {
|
|
87
91
|
if (raw === undefined) return { everyHours: COSTS_SNAPSHOT_EVERY_HOURS.default };
|
|
88
92
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
89
93
|
throw new Error("costs.snapshot must be a mapping");
|
|
90
|
-
const
|
|
91
|
-
|
|
94
|
+
const r = raw as Record<string, unknown>;
|
|
95
|
+
// `undefined` alone defaults: a bare `everyHours:` key (null) is a malformed value, refused below by name.
|
|
96
|
+
const every = r.everyHours === undefined ? COSTS_SNAPSHOT_EVERY_HOURS.default : r.everyHours;
|
|
92
97
|
if (
|
|
93
98
|
typeof every !== "number" ||
|
|
94
99
|
!Number.isInteger(every) ||
|
|
@@ -98,7 +103,10 @@ function snapshotConfig(raw: unknown): CostsConfig["snapshot"] {
|
|
|
98
103
|
throw new Error(
|
|
99
104
|
`costs.snapshot.everyHours must be a whole number of hours between ${COSTS_SNAPSHOT_EVERY_HOURS.min} and ${COSTS_SNAPSHOT_EVERY_HOURS.max}`,
|
|
100
105
|
);
|
|
101
|
-
return { everyHours: every };
|
|
106
|
+
if (r.alertChannel === undefined) return { everyHours: every };
|
|
107
|
+
if (typeof r.alertChannel !== "string" || !/^[a-z]+:.+$/.test(r.alertChannel))
|
|
108
|
+
throw new Error("costs.snapshot.alertChannel must be a platform-namespaced channel id (`slack:C…`)");
|
|
109
|
+
return { everyHours: every, alertChannel: r.alertChannel };
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
function labelMap(raw: unknown, what: string): Record<string, string> {
|
|
@@ -762,10 +770,15 @@ export function anthropicTokensCostUsd(modelId: string, t: AnthropicTokens): num
|
|
|
762
770
|
// ---- range ----------------------------------------------------------------------------
|
|
763
771
|
|
|
764
772
|
const DEFAULT_DAYS = 30;
|
|
765
|
-
/** The widest range the page offers — and the window a costs snapshot is read for.
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
773
|
+
/** The widest range the page offers — and the window a costs snapshot is read for.
|
|
774
|
+
* Cloudflare's analytics on a Workers account answer no range wider than 4w4d
|
|
775
|
+
* (32 days) and hold no data older than that (`cannot request a time range
|
|
776
|
+
* wider than 4w4d`, `cannot request data older than 4w4d` — measured live), so
|
|
777
|
+
* a wider window is a refused read, not more history: 31 UTC days is the most
|
|
778
|
+
* a take can ask for. */
|
|
779
|
+
export const MAX_DAYS = 31;
|
|
780
|
+
|
|
781
|
+
/** `?days=N` → a UTC date range ending today. Garbage → default; clamped 1..31. */
|
|
769
782
|
export function resolveRange(daysParam: string | null, now: Date = new Date(systemClock())): DateRange {
|
|
770
783
|
const parsed = daysParam === null ? NaN : Number(daysParam);
|
|
771
784
|
const days = Number.isInteger(parsed) ? Math.min(MAX_DAYS, Math.max(1, parsed)) : DEFAULT_DAYS;
|
|
@@ -1070,7 +1083,7 @@ export class AnthropicCostReportSource implements LlmCostSource {
|
|
|
1070
1083
|
if (range.from >= endExclusive) return { rows, closedThrough };
|
|
1071
1084
|
let page: string | null = null;
|
|
1072
1085
|
for (let i = 0; ; i++) {
|
|
1073
|
-
// Like the non-USD check: refuse rather than mis-sum. A ≤
|
|
1086
|
+
// Like the non-USD check: refuse rather than mis-sum. A ≤31-day range at
|
|
1074
1087
|
// limit=31 is at most 3 pages, so hitting the cap means the API changed.
|
|
1075
1088
|
if (i >= MAX_COST_PAGES)
|
|
1076
1089
|
throw new Error(
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Where a preset's harness word is set (docs/reference/specs/harness.md item
|
|
2
|
+
// 8), in a module with no imports of its own: the run record (`run_meta.harnessScope`)
|
|
3
|
+
// and the timeline fold read it, and both are compiled into the Workers' and
|
|
4
|
+
// the dashboard's programs, where the roster's neighbours — the harness
|
|
5
|
+
// objects, their Node-only process code — must not follow. The roster
|
|
6
|
+
// re-exports these names, so every reader inside the bot can still take them
|
|
7
|
+
// from there.
|
|
8
|
+
|
|
9
|
+
/** The scopes a word is set at, most specific first — the order the resolution
|
|
10
|
+
* walks the layers: the requester's own scope, the channel's, then the
|
|
11
|
+
* deployment's top-level `harness` block, which is the defaults layer under
|
|
12
|
+
* its one spelling. What `run_meta.harnessScope` and the config block name, so
|
|
13
|
+
* a reader of a run on OpenCode never guesses whose word put it there. */
|
|
14
|
+
export const HARNESS_SCOPES = ["user", "channel", "defaults"] as const;
|
|
15
|
+
export type HarnessScope = (typeof HARNESS_SCOPES)[number];
|
|
16
|
+
|
|
17
|
+
/** Whether a value is one of the scopes a word is set at: the timeline's test
|
|
18
|
+
* for the field a record carries. */
|
|
19
|
+
export function isHarnessScope(value: unknown): value is HarnessScope {
|
|
20
|
+
return typeof value === "string" && (HARNESS_SCOPES as readonly string[]).includes(value);
|
|
21
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// the node-free contract the memory Worker and web app compile with their own
|
|
3
3
|
// tsconfigs — importing prDescription.ts would drag zod into those graphs.
|
|
4
4
|
import type { PrDescription, RenderedTourStep } from "./prDescriptionTypes.js";
|
|
5
|
+
import type { HarnessScope } from "./harness/scope.js";
|
|
5
6
|
|
|
6
7
|
/** The `pr_description` review artifact minus the event envelope
|
|
7
8
|
* (docs/reference/specs/reading-diff.md item 7). */
|
|
@@ -97,6 +98,11 @@ export type RunNoteKind =
|
|
|
97
98
|
/** The PR head moved while a review ran and the same run is re-reviewing at
|
|
98
99
|
* the new head (agent-review.md item 12). Published by the dispatcher. */
|
|
99
100
|
| "head_moved"
|
|
101
|
+
/** The run loop threw and the run finishes `failed`: the summary is the
|
|
102
|
+
* error's message, redacted and capped, so the run page says why a failed
|
|
103
|
+
* run failed even when the reply is never delivered (run-history.md).
|
|
104
|
+
* Published by the run loop's catch, before the finish. */
|
|
105
|
+
| "run_failed"
|
|
100
106
|
/** An MCP server configured for this agent did not answer discovery
|
|
101
107
|
* (docs/reference/specs/mcp-tools.md item 8); the run proceeds without its tools. One
|
|
102
108
|
* note per server, published by the dispatcher before the first turn. */
|
|
@@ -187,6 +193,11 @@ export type RunNoteKind =
|
|
|
187
193
|
* item 7): the summary names the tool and the rule; the model read the same
|
|
188
194
|
* reason as the tool's result. Published by the bot's authorize route. */
|
|
189
195
|
| "tool_refused"
|
|
196
|
+
/** A ship coding child's budget ended with work still in the tree: the run
|
|
197
|
+
* loop committed and pushed it to the unit's branch (or says plainly that
|
|
198
|
+
* there was nothing to push), so a re-issue starts from the partial work
|
|
199
|
+
* (docs/reference/specs/agent-ship.md item 8). */
|
|
200
|
+
| "budget_salvage"
|
|
190
201
|
/** The native loop's stuck-loop guard: the same tool call failed identically
|
|
191
202
|
* six times in a row and the run was forced into its write-up. Written by
|
|
192
203
|
* no loop since that loop's deletion; a record from before it may carry it. */
|
|
@@ -205,6 +216,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
205
216
|
"stopped",
|
|
206
217
|
"spans_dropped",
|
|
207
218
|
"head_moved",
|
|
219
|
+
"run_failed",
|
|
208
220
|
"mcp_unavailable",
|
|
209
221
|
"follow_up",
|
|
210
222
|
"resumed",
|
|
@@ -220,6 +232,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
220
232
|
"harness_error",
|
|
221
233
|
"policy_refusal",
|
|
222
234
|
"tool_refused",
|
|
235
|
+
"budget_salvage",
|
|
223
236
|
"stuck_loop",
|
|
224
237
|
] as const satisfies readonly RunNoteKind[];
|
|
225
238
|
type _EveryKindListed = [RunNoteKind] extends [(typeof RUN_NOTE_KINDS)[number]] ? true : never;
|
|
@@ -519,9 +532,15 @@ export type RunEvent =
|
|
|
519
532
|
/** The request's trace id (docs/reference/specs/tracing.md), once the root exists. */
|
|
520
533
|
traceId?: string;
|
|
521
534
|
/** The harness the run is driven by (`Harness.name`; docs/reference/specs/harness.md
|
|
522
|
-
*
|
|
535
|
+
* items 8 and 10): the word the scopes resolved for the preset, `pi`
|
|
536
|
+
* when none named it. Absent on a command run, which starts no process,
|
|
523
537
|
* and on a record written before the seam existed. Additive. */
|
|
524
538
|
harness?: string;
|
|
539
|
+
/** Whose word put the run on that harness (item 10): the requester's own
|
|
540
|
+
* scope, the channel's, or the deployment's top-level block. Absent when
|
|
541
|
+
* no scope named the preset — the roster's default — and on records
|
|
542
|
+
* written before the word was a scope setting. */
|
|
543
|
+
harnessScope?: HarnessScope;
|
|
525
544
|
effort?: string;
|
|
526
545
|
repo?: string;
|
|
527
546
|
ref?: string;
|
|
@@ -56,6 +56,8 @@ export interface LiveRunMeta {
|
|
|
56
56
|
/** The bound credential behind the person (authorization.md item 15): a
|
|
57
57
|
* resume or restart dispatches under ITS grants again, never the person's. */
|
|
58
58
|
authenticatedAs?: string;
|
|
59
|
+
/** The app that relayed the request for the person (authorization.md item 14): a resume or restart keeps app ∩ person at the gates. */
|
|
60
|
+
postedBy?: string;
|
|
59
61
|
effort?: string;
|
|
60
62
|
ref?: string;
|
|
61
63
|
headSha?: string;
|
|
@@ -109,6 +109,9 @@ export interface RunRecord {
|
|
|
109
109
|
receivedAt?: number;
|
|
110
110
|
sealedAt?: number;
|
|
111
111
|
replyOk?: boolean;
|
|
112
|
+
/** Why a `replyOk: false` reply was not delivered (e.g. "no channel to deliver
|
|
113
|
+
* to", run-history.md item 38). Absent when the reply was delivered or none was made. */
|
|
114
|
+
replyNote?: string;
|
|
112
115
|
stepCount?: number;
|
|
113
116
|
schema?: number;
|
|
114
117
|
status: RunStatus;
|
|
@@ -783,6 +786,7 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
783
786
|
if (r[key] !== undefined && !isFiniteNumber(r[key])) return false;
|
|
784
787
|
}
|
|
785
788
|
if (r.replyOk !== undefined && typeof r.replyOk !== "boolean") return false;
|
|
789
|
+
if (r.replyNote !== undefined && typeof r.replyNote !== "string") return false;
|
|
786
790
|
for (const key of ["stepCount", "schema"] as const) {
|
|
787
791
|
if (r[key] !== undefined && (!isFiniteNumber(r[key]) || !Number.isInteger(r[key]) || (r[key] as number) < 0))
|
|
788
792
|
return false;
|
|
@@ -428,8 +428,10 @@ function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
|
|
|
428
428
|
return (
|
|
429
429
|
`Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
|
|
430
430
|
`the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
|
|
431
|
-
`
|
|
432
|
-
`
|
|
431
|
+
`Push the branch as soon as the change exists and its cheapest proving checks pass — before the project's ` +
|
|
432
|
+
`full verification, which runs after that push with any fix as a further commit; an unpushed tree does not ` +
|
|
433
|
+
`survive the run's end. Right before each push, fetch ${onto} again and rebase once more if it moved while ` +
|
|
434
|
+
`you worked, so the pull request is not born conflicting.`
|
|
433
435
|
);
|
|
434
436
|
}
|
|
435
437
|
|
|
@@ -448,6 +448,8 @@ export type CoordinatorAction =
|
|
|
448
448
|
recover?: { runId: string };
|
|
449
449
|
}
|
|
450
450
|
| { type: "merge"; step: string; prNumber: number; headSha: string }
|
|
451
|
+
/** Wait for the intake's checks-settled event at the approved head, bounded as the fallback. */
|
|
452
|
+
| { type: "wait-checks"; step: string; headSha: string; timeoutMs: number }
|
|
451
453
|
| { type: "sleep"; step: string; ms: number }
|
|
452
454
|
| { type: "end"; step: string; ending: UnitEnding };
|
|
453
455
|
|
|
@@ -508,6 +510,7 @@ export type StepReturn =
|
|
|
508
510
|
// fired, or a person merged — so the runner merged nothing (`by: other`).
|
|
509
511
|
| { type: "merge"; step: string; outcome: "merged"; by: "other"; sha: string; mergedAt: string; at: number }
|
|
510
512
|
| { type: "merge"; step: string; outcome: "pending" | "refused"; reason: string; at: number }
|
|
513
|
+
| { type: "wait-checks"; step: string; outcome: "event" | "timeout" }
|
|
511
514
|
| { type: "sleep"; step: string };
|
|
512
515
|
|
|
513
516
|
/** How one unit's pipeline ended — the truthful vocabulary the ship pipeline
|
|
@@ -628,7 +631,7 @@ type Phase =
|
|
|
628
631
|
dead?: "failed" | "interrupted";
|
|
629
632
|
}
|
|
630
633
|
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number }
|
|
631
|
-
| { at: "merge-
|
|
634
|
+
| { at: "merge-wait"; pr: PrRef; headSha: string; n: number; since: number }
|
|
632
635
|
| { at: "ended" };
|
|
633
636
|
|
|
634
637
|
export interface UnitPipelineState {
|
|
@@ -668,9 +671,16 @@ export const WAIT_MARGIN_MS = 5 * MIN;
|
|
|
668
671
|
* margin and the merge poll are the same number), and it keeps a round to a
|
|
669
672
|
* few steps: a coding child's 45 minutes are ten waits and ten reads. */
|
|
670
673
|
export const WAIT_CHUNK_MS = 5 * MIN;
|
|
671
|
-
/** How
|
|
672
|
-
|
|
674
|
+
/** How long the merge step waits for the guards at most. While checks are
|
|
675
|
+
* pending the machine waits on the intake's `checks-settled-<head>` event
|
|
676
|
+
* (http-ingress.md item 12) — one bounded wait per ask, the remainder of this
|
|
677
|
+
* cap, as the fallback when the event never arrives. */
|
|
673
678
|
export const MERGE_WAIT_MAX_MS = 60 * MIN;
|
|
679
|
+
/** One merge wait's fallback timeout: the old poll's cadence. The event wakes
|
|
680
|
+
* the machine at once when the intake delivers it; without one (the webhook
|
|
681
|
+
* not configured, a delivery lost) the door is still re-asked every chunk, so
|
|
682
|
+
* a merge is never slower than the poll it replaced. */
|
|
683
|
+
export const MERGE_WAIT_CHUNK_MS = 5 * MIN;
|
|
674
684
|
/** A `busy` without the live run's id: nothing to wait on, so a short sleep before the spawn is asked again. */
|
|
675
685
|
export const BUSY_RETRY_MS = 2 * MIN;
|
|
676
686
|
|
|
@@ -816,8 +826,13 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
816
826
|
};
|
|
817
827
|
case "merge":
|
|
818
828
|
return { type: "merge", step: `${unit}/merge/${p.n}`, prNumber: p.pr.number, headSha: p.headSha };
|
|
819
|
-
case "merge-
|
|
820
|
-
return {
|
|
829
|
+
case "merge-wait":
|
|
830
|
+
return {
|
|
831
|
+
type: "wait-checks",
|
|
832
|
+
step: `${unit}/merge/wait/${p.n}`,
|
|
833
|
+
headSha: p.headSha,
|
|
834
|
+
timeoutMs: Math.max(MIN, Math.min(MERGE_WAIT_CHUNK_MS, MERGE_WAIT_MAX_MS - (s.clock - p.since))),
|
|
835
|
+
};
|
|
821
836
|
case "ended":
|
|
822
837
|
return { type: "end", step: `${unit}/end`, ending: s.ending! };
|
|
823
838
|
}
|
|
@@ -1349,11 +1364,11 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1349
1364
|
reviewRounds: s.reviewRounds,
|
|
1350
1365
|
});
|
|
1351
1366
|
return {
|
|
1352
|
-
state: { ...clocked, phase: { at: "merge-
|
|
1367
|
+
state: { ...clocked, phase: { at: "merge-wait", pr: p.pr, headSha: p.headSha, n: p.n, since: p.since } },
|
|
1353
1368
|
notes: [],
|
|
1354
1369
|
};
|
|
1355
1370
|
}
|
|
1356
|
-
case "merge-
|
|
1371
|
+
case "merge-wait":
|
|
1357
1372
|
return {
|
|
1358
1373
|
state: { ...s, phase: { at: "merge", pr: p.pr, headSha: p.headSha, n: p.n + 1, since: p.since } },
|
|
1359
1374
|
notes: [],
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* field. */
|
|
9
9
|
|
|
10
10
|
export type Backend = "local" | "resident" | "sandbox" | "e2b";
|
|
11
|
-
export type Channel = "slack" | "http" | "mcp" | "cli";
|
|
11
|
+
export type Channel = "slack" | "http" | "mcp" | "cli" | "web";
|
|
12
12
|
|
|
13
13
|
/** Every attribute key any span may carry, with its value domain. */
|
|
14
14
|
export interface AttrDomain {
|
|
@@ -14,8 +14,8 @@ export interface PurgeableBinding {
|
|
|
14
14
|
export type PurgeDecision = { ok: true; purge: string[]; keptLive: string[] } | { ok: false; error: string };
|
|
15
15
|
|
|
16
16
|
/** Thread-key namespaces real channels mint (docs/reference/specs/http-ingress.md item 1,
|
|
17
|
-
* slack, mcp, the CLI). A purge is for synthetic keys only. */
|
|
18
|
-
const PRODUCTION_NAMESPACES = new Set(["slack", "http", "mcp", "cli"]);
|
|
17
|
+
* slack, mcp, the CLI, the web chat). A purge is for synthetic keys only. */
|
|
18
|
+
const PRODUCTION_NAMESPACES = new Set(["slack", "http", "mcp", "cli", "web"]);
|
|
19
19
|
|
|
20
20
|
/** A prefix must be a whole namespace (`load:`) or longer (`load:r1:`), never
|
|
21
21
|
* empty, never a bare partial namespace, never a production namespace. */
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* cycle, and these say the snapshot (or the container that would restore it)
|
|
10
10
|
* cannot be used — never a provision failure, which would loop against the
|
|
11
11
|
* same broken build. `runtime-unreachable` is item 64's last rung: a recreated
|
|
12
|
-
* container that did not answer either
|
|
13
|
-
|
|
12
|
+
* container that did not answer either; `infra-streak` is item 67's: a
|
|
13
|
+
* recreated container whose cycles kept failing in the resident's own steps. */
|
|
14
|
+
export const REHYDRATION_FAILURE_RE =
|
|
15
|
+
/^(r2-restore-failed|snapshot-stamp-mismatch|no-snapshot|runtime-unreachable|infra-streak)/;
|
|
14
16
|
|
|
15
17
|
/** How many auto-rebuilds one resident gets inside one window. A rebuild is a
|
|
16
18
|
* clone, an install and a build (minutes to half an hour) that holds the cap
|
|
@@ -67,10 +67,9 @@ export type FetchReason = "missing-ref" | "stale-tip" | "returnable-ref";
|
|
|
67
67
|
* lost and a ref a person named that the thread never pushed has no way
|
|
68
68
|
* back, so neither pays this fetch: the refresh cycle is their freshness.
|
|
69
69
|
* - otherwise null.
|
|
70
|
-
* A fetch that STILL leaves the tip elsewhere (a push racing this attach,
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* review of it may do. */
|
|
70
|
+
* A fetch that STILL leaves the tip elsewhere (a push racing this attach, a
|
|
71
|
+
* force-push, or a fetch that failed) is `attachTarget`'s concern: the attach
|
|
72
|
+
* is refused `stale-tip` rather than run at a commit nobody asked for. */
|
|
74
73
|
export function mirrorFetchReason(input: {
|
|
75
74
|
refExists: boolean;
|
|
76
75
|
mirrorSha?: string;
|
|
@@ -85,8 +84,16 @@ export function mirrorFetchReason(input: {
|
|
|
85
84
|
|
|
86
85
|
/** What the attach checks out, once the mirror is as fresh as it will get. */
|
|
87
86
|
export type AttachTarget =
|
|
88
|
-
/** The bound ref is in the mirror
|
|
87
|
+
/** The bound ref is in the mirror, and its tip is the commit the caller
|
|
88
|
+
* named (or the caller named none): clone its tip, as always. */
|
|
89
89
|
| { kind: "ref" }
|
|
90
|
+
/** The bound ref is in the mirror but its tip is NOT the commit the caller
|
|
91
|
+
* named, even after the fetch — the mirror is behind it (the fetch failed
|
|
92
|
+
* or was skipped) or ahead of it (a push raced the attach). Refused: a run
|
|
93
|
+
* executes at the sha it asked for, or not on this resident; the caller
|
|
94
|
+
* falls back cold at the requested commit. `tip` is null when the tip
|
|
95
|
+
* could not be read — never assumed fresh. */
|
|
96
|
+
| { kind: "stale-tip"; tip: string | null; want: string }
|
|
90
97
|
/** The ref is gone but the commit the caller expects is in the mirror — a
|
|
91
98
|
* merged PR's branch was deleted while `refs/pull/N/head` (a `--mirror`
|
|
92
99
|
* clone carries every ref) still holds its head: check that commit out,
|
|
@@ -96,16 +103,22 @@ export type AttachTarget =
|
|
|
96
103
|
/** Neither: the attach is refused as `unknown-ref`. */
|
|
97
104
|
| { kind: "unknown-ref" };
|
|
98
105
|
|
|
99
|
-
/** The attach target after the fetch (item 51).
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
106
|
+
/** The attach target after the fetch (item 51). A ref that exists is cloned
|
|
107
|
+
* at its tip when the caller named no commit or the tip is that commit;
|
|
108
|
+
* a tip that is any other commit is `stale-tip`, refused. A detached tree is
|
|
109
|
+
* only for a ref that is gone; a caller that named no commit, or whose commit
|
|
110
|
+
* the mirror does not hold either, gets the refusal it always got. */
|
|
103
111
|
export function attachTarget(input: {
|
|
104
112
|
refExists: boolean;
|
|
105
113
|
wantSha: string | null;
|
|
106
114
|
commitInMirror: boolean;
|
|
115
|
+
/** The ref's tip in the mirror after the fetch; null or absent when it could not be read. */
|
|
116
|
+
tipSha?: string | null;
|
|
107
117
|
}): AttachTarget {
|
|
108
|
-
if (input.refExists)
|
|
118
|
+
if (input.refExists) {
|
|
119
|
+
if (input.wantSha === null || input.tipSha === input.wantSha) return { kind: "ref" };
|
|
120
|
+
return { kind: "stale-tip", tip: input.tipSha ?? null, want: input.wantSha };
|
|
121
|
+
}
|
|
109
122
|
if (input.wantSha !== null && input.commitInMirror) return { kind: "sha", sha: input.wantSha };
|
|
110
123
|
return { kind: "unknown-ref" };
|
|
111
124
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// A refresh cycle that keeps failing in the resident's OWN steps heals itself
|
|
2
|
+
// (docs/reference/specs/resident-repos.md item 67). Pure: the Worker's
|
|
3
|
+
// `refreshFailed` bumps the row and acts on the rung; the cycle's gate asks
|
|
4
|
+
// `parksOnRepeat` before it counts a degraded reason toward the park streak.
|
|
5
|
+
//
|
|
6
|
+
// The split this module draws: a step that runs the repository's own command
|
|
7
|
+
// (the onboard-time command table — install, build, test) failing is evidence
|
|
8
|
+
// about the repository, and the right response is to park and wait for the
|
|
9
|
+
// head to be fixed, which is what the park streak does. Every other step is
|
|
10
|
+
// the resident's machinery — git against the mirror, the probes, the markers,
|
|
11
|
+
// the restores — and a failure there that repeats is the container or the
|
|
12
|
+
// disk gone wrong, which a rebuild does fix. Item 64 already climbs this
|
|
13
|
+
// ladder for one signature (the control port that never answers); this is
|
|
14
|
+
// the same ladder for every other resident-step failure.
|
|
15
|
+
|
|
16
|
+
import { STALE_SWEEP_SUFFIX } from "./residentSteps.js";
|
|
17
|
+
|
|
18
|
+
/** The steps that run the repository's own commands. */
|
|
19
|
+
export const REPO_COMMAND_STEPS: ReadonlySet<string> = new Set(["deps-install", "build", "test"]);
|
|
20
|
+
|
|
21
|
+
/** Whether a failed step ran the repository's command (evidence about the
|
|
22
|
+
* repo) rather than the resident's own machinery. The stale-process sweep
|
|
23
|
+
* that precedes a repo step is ours. */
|
|
24
|
+
export function isRepoCommandStep(step: string): boolean {
|
|
25
|
+
if (step.endsWith(STALE_SWEEP_SUFFIX)) return false;
|
|
26
|
+
return REPO_COMMAND_STEPS.has(step);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The step named by a `<step>-failed:` reason (`classifyRefreshFailure`'s
|
|
30
|
+
* plain-failure shape), or null for every other reason. */
|
|
31
|
+
export function stepOfFailedReason(reason: string): string | null {
|
|
32
|
+
const m = /^([a-z][a-z0-9-]*)-failed:/.exec(reason);
|
|
33
|
+
return m ? m[1] : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Whether a repeated degraded reason should PARK the resident (the 6-hour
|
|
37
|
+
* cadence, `DEGRADED_PARK_AFTER_CYCLES`): only evidence about the repository
|
|
38
|
+
* or about GitHub — a repo-command step's failure, or a reason that names no
|
|
39
|
+
* step at all (`github-unreachable`). A resident-step failure never parks:
|
|
40
|
+
* the ladder below is its escalation, and parking would only slow it. */
|
|
41
|
+
export function parksOnRepeat(reason: string): boolean {
|
|
42
|
+
const step = stepOfFailedReason(reason);
|
|
43
|
+
return step === null || isRepoCommandStep(step);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The ladder's rungs: consecutive cycles failed in resident steps. */
|
|
47
|
+
export const INFRA_STREAK_RECREATE_AT = 3;
|
|
48
|
+
export const INFRA_STREAK_DOWN_AT = 5;
|
|
49
|
+
|
|
50
|
+
export type InfraStreakRung = "count" | "recreate" | "down";
|
|
51
|
+
|
|
52
|
+
/** Count 1–2: record and let the next cycle try. Count 3: destroy the
|
|
53
|
+
* container, snapshots kept — the next cycle restores from the snapshot onto
|
|
54
|
+
* a fresh disk (item 64's rung 3). Count 4: the recreated container's one
|
|
55
|
+
* cycle of its own. Count ≥ 5: a fresh container failed the same way; `down`
|
|
56
|
+
* with a rehydration-flavored reason, so item 36's transition rebuild is the
|
|
57
|
+
* exit. */
|
|
58
|
+
export function infraStreakRung(count: number): InfraStreakRung {
|
|
59
|
+
if (!Number.isFinite(count) || count < 1) {
|
|
60
|
+
throw new RangeError(`infra-streak rung needs a count of at least 1, got ${count}`);
|
|
61
|
+
}
|
|
62
|
+
if (count >= INFRA_STREAK_DOWN_AT) return "down";
|
|
63
|
+
if (count === INFRA_STREAK_RECREATE_AT) return "recreate";
|
|
64
|
+
return "count";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The persisted row (`resident:infraStreak`): the last failing step, the
|
|
68
|
+
* consecutive count and the span. */
|
|
69
|
+
export interface InfraStreakRow {
|
|
70
|
+
step: string;
|
|
71
|
+
count: number;
|
|
72
|
+
firstAt: string;
|
|
73
|
+
lastAt: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const INFRA_STREAK_ACTION: Readonly<Record<Exclude<InfraStreakRung, "count">, string>> = {
|
|
77
|
+
recreate: "the container was destroyed, snapshots kept; the next cycle restores from the snapshot",
|
|
78
|
+
down: "a recreated container failed the same way; rebuilding",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** The reason a rung records: `infra-streak:` so the gate, the serviceability
|
|
82
|
+
* check and item 36's eligibility all read it as the resident's, never the repo's. */
|
|
83
|
+
export function infraStreakReason(row: InfraStreakRow, rung: Exclude<InfraStreakRung, "count">): string {
|
|
84
|
+
return `infra-streak: ${row.count} consecutive cycles failed in the resident's own steps (last: ${row.step}, since ${row.firstAt}) — ${INFRA_STREAK_ACTION[rung]}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isInfraStreakReason(reason: string): boolean {
|
|
88
|
+
return /^infra-streak: /.test(reason);
|
|
89
|
+
}
|
|
@@ -132,6 +132,10 @@ export interface RefreshFailure {
|
|
|
132
132
|
* with the attempt count (the ladder below decides what the resident does
|
|
133
133
|
* about it); real failures keep `<step>-failed:`. */
|
|
134
134
|
reason: string;
|
|
135
|
+
/** The step that failed, as the caller named it (`refresh` for a failure
|
|
136
|
+
* between steps): item 67 reads whether it ran the repository's own command
|
|
137
|
+
* or the resident's machinery. */
|
|
138
|
+
step: string;
|
|
135
139
|
interrupted: boolean;
|
|
136
140
|
diskFull: boolean;
|
|
137
141
|
runtimeUnreachable: boolean;
|
|
@@ -234,6 +238,7 @@ export function classifyRefreshFailure(input: {
|
|
|
234
238
|
const { step, message } = input;
|
|
235
239
|
if (input.runtimeUnreachable) {
|
|
236
240
|
return {
|
|
241
|
+
step,
|
|
237
242
|
interrupted: false,
|
|
238
243
|
diskFull: false,
|
|
239
244
|
runtimeUnreachable: true,
|
|
@@ -242,6 +247,7 @@ export function classifyRefreshFailure(input: {
|
|
|
242
247
|
}
|
|
243
248
|
if (isDiskFullMessage(message)) {
|
|
244
249
|
return {
|
|
250
|
+
step,
|
|
245
251
|
interrupted: false,
|
|
246
252
|
diskFull: true,
|
|
247
253
|
runtimeUnreachable: false,
|
|
@@ -251,6 +257,7 @@ export function classifyRefreshFailure(input: {
|
|
|
251
257
|
const timedOut = /\(timed out\)/.test(message);
|
|
252
258
|
if (!timedOut && (INTERRUPTION_SIGNATURE.test(message) || RUNTIME_REPLACEMENT_WORDING.test(message))) {
|
|
253
259
|
return {
|
|
260
|
+
step,
|
|
254
261
|
interrupted: true,
|
|
255
262
|
diskFull: false,
|
|
256
263
|
runtimeUnreachable: false,
|
|
@@ -259,13 +266,20 @@ export function classifyRefreshFailure(input: {
|
|
|
259
266
|
}
|
|
260
267
|
if (input.freeKiB !== undefined && input.freeKiB !== null && input.freeKiB < DISK_FULL_FREE_KIB) {
|
|
261
268
|
return {
|
|
269
|
+
step,
|
|
262
270
|
interrupted: false,
|
|
263
271
|
diskFull: true,
|
|
264
272
|
runtimeUnreachable: false,
|
|
265
273
|
reason: diskFullReason({ step, message, freeKiB: input.freeKiB }),
|
|
266
274
|
};
|
|
267
275
|
}
|
|
268
|
-
return {
|
|
276
|
+
return {
|
|
277
|
+
step,
|
|
278
|
+
interrupted: false,
|
|
279
|
+
diskFull: false,
|
|
280
|
+
runtimeUnreachable: false,
|
|
281
|
+
reason: `${step}-failed: ${message}`,
|
|
282
|
+
};
|
|
269
283
|
}
|
|
270
284
|
|
|
271
285
|
// -- the runtime that never answers --------------------------------------------
|
|
@@ -49,6 +49,7 @@ export const RESIDENT_STEP_LABELS = {
|
|
|
49
49
|
"worktree-clean": "cleaning the worktree",
|
|
50
50
|
"clean-workspace": "cleaning the workspace",
|
|
51
51
|
"clean-before-restore": "cleaning before the restore",
|
|
52
|
+
"break-mirror": "removing the mirror's objects (fault injection)",
|
|
52
53
|
"unmount-restores": "unmounting earlier restores",
|
|
53
54
|
"mirror-restore-extract": "extracting the mirror snapshot",
|
|
54
55
|
"checkout-restore-extract": "extracting the checkout snapshot",
|