@coreplane/switchboard 1.245.0 → 1.247.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 +1 -1
- package/dist/assets/deploy/cloudflare-memory/worker.ts +14 -4
- package/dist/assets/deploy/cloudflare-resident/worker.ts +10 -11
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +17 -1
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +5 -3
- package/dist/assets/project.json +10 -2
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +32 -8
- package/dist/assets/src/core/budgets.ts +37 -7
- package/dist/assets/src/core/coordinator/contract.ts +29 -4
- package/dist/assets/src/core/coordinator/driver.ts +7 -1
- package/dist/assets/src/core/reviewVerdict.ts +138 -6
- package/dist/assets/src/core/runEvents.ts +34 -2
- package/dist/assets/src/core/runRecord.ts +38 -16
- package/dist/assets/src/core/ship/contract.ts +7 -0
- package/dist/assets/src/core/ship/coordinator.ts +24 -3
- package/dist/assets/src/core/ship/renewal.ts +10 -3
- package/dist/assets/src/core/trace/attrs.ts +9 -0
- package/dist/assets/src/execution/bashTimeout.ts +46 -2
- package/dist/assets/src/execution/residentRefresh.ts +9 -5
- package/dist/assets/src/execution/sandboxErrors.ts +9 -4
- package/dist/assets/web/dist/.vite/manifest.json +474 -485
- package/dist/assets/web/dist/assets/AppShell-DAtHiuI6.js +1 -0
- package/dist/assets/web/dist/assets/CostsPage-BoKzYa4B.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-NP4g6bQd.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-DYxC0izY.js +2 -0
- package/dist/assets/web/dist/assets/InputMenu-C6aPpe30.js +1 -0
- package/dist/assets/web/dist/assets/NotFoundPage-DtE-GgTk.js +1 -0
- package/dist/assets/web/dist/assets/PendingTurnRow-BuRre8it.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-DLIpWYOc.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-6LipuDjR.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-V-iSy64e.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-DUalB1u2.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-B9Ba1KdD.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-YSbUu5py.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-KdjLtD_7.js +1 -0
- package/dist/assets/web/dist/assets/SettingSelect-BIzAsLk1.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-IT5l_NaL.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-DBHAl4Il.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-_LEjptLV.js +1 -0
- package/dist/assets/web/dist/assets/UnitRoutePage-DPBsvGPR.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-qXhb9ti8.js → angular-html-BjeQZdCq.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DzvdyoUZ.js → angular-ts-ZGdORrL2.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-Ck7Ru2Sm.js → apl-DhvV_X93.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-CwehvReb.js → astro-DiRSE4Ug.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-WSyVbSFK.js → blade-D8Fr5Wvq.js} +1 -1
- package/dist/assets/web/dist/assets/{seed-DFfT5S_N.js → browser-B6T5hjfR.js} +2 -2
- package/dist/assets/web/dist/assets/{c-DMHI1wDz.js → c-BwbC64D4.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-BF8AYK8_.js → chapel-DgmqRK97.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-UAfImuWu.js → cobol-DJod2RNK.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-D54hi96d.js → coffee-D1Y2CgBY.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-BRWfLgBc.js → cpp-CsO97YOM.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CSqC5deT.js → crystal-CJ7m2tOc.js} +1 -1
- package/dist/assets/web/dist/assets/{css-D4ZuRFGk.js → css-B2M-NKoj.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-73ZMn_Mg.js → dist-BcYPGOBL.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +1 -0
- package/dist/assets/web/dist/assets/{edge-DDPtNbZc.js → edge-CFVW-m9B.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-BTQkQfsJ.js → elixir-B2mtRgCr.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-B_PkLzGZ.js → elm-2teVAwCi.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-DfjIMVM_.js → erb-LAbeNqen.js} +1 -1
- package/dist/assets/web/dist/assets/eventSource-Bj_dZf6M.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-DP8wmn7K.js → git-rebase-DnhzH_sp.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-G12VK2gK.js → glimmer-js-DPKjXf1-.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-D3XiSI8E.js → glimmer-ts-CABXnt5z.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-DOKAhMj-.js → glsl-Dz4-3-gJ.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-CJB_fmON.js → graphql-ByWUD1DL.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BHqtRWap.js → hack-CdmOK2K-.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DwotyarS.js → haml-DzegMpd2.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-B6fDaA3U.js → handlebars-B5NeiP0e.js} +1 -1
- package/dist/assets/web/dist/assets/{html-s52taj36.js → html-Zg_NHLv9.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-C5bpQmmI.js → html-derivative-DZf9JNzh.js} +1 -1
- package/dist/assets/web/dist/assets/{http-99zC935r.js → http-CVFrQZeN.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-Bgk-4-0x.js → hurl-L5brWAJe.js} +1 -1
- package/dist/assets/web/dist/assets/{java-CuV_c5I0.js → java-hFphyZ4R.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CZlOvSMQ.js → javascript-CAJO1WKX.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-BuXeZ4Tu.js → jinja-N4A4srXI.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DIuP0hkG.js → jison-D3BZa_iU.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CZ74vfoD.js → json-DxD1Qh8W.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-BNjRs8Vj.js → jsx-CHhag69S.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-OCE-fFOg.js → julia-CrhkZ6Tt.js} +1 -1
- package/dist/assets/web/dist/assets/{just-CtvD87WF.js → just-D0LFOuhq.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-C3kf86OR.js → latex-BHAlwCXK.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-B1WI--Kp.js → liquid-CkaLfCWh.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-DtoT0gKg.js → lua-B1h4aAzP.js} +1 -1
- package/dist/assets/web/dist/assets/main-DUfSE0dj.js +28 -0
- package/dist/assets/web/dist/assets/main-zbP_dTjR.css +1 -0
- package/dist/assets/web/dist/assets/{marko-BE2MC6Yt.js → marko-D6CG4hvX.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-BwO4weXJ.js → mdc-CVQzimAU.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-BTuOgGVC.js → nginx-h-9Ir54h.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-CiIUM-oV.js → nim-DHVI30sp.js} +1 -1
- package/dist/assets/web/dist/assets/{org-L78DEWB-.js → org-AasAXfMF.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-KqHOm4Ft.js → perl-BVi5aTEy.js} +1 -1
- package/dist/assets/web/dist/assets/{php-C41XbEkn.js → php-qOkCeUKr.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-B12-4i-k.js → pug-8TnUGvEX.js} +1 -1
- package/dist/assets/web/dist/assets/{qml--fw2Up5c.js → qml-CpJbhoxn.js} +1 -1
- package/dist/assets/web/dist/assets/{r-Db1JBXrC.js → r-JkUNcAJW.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BYpVBgbu.js → razor-CtU4ws5a.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-BMqGLSPc.js → regexp-J2zA6ceN.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-B63n7-Sa.js → rst-CMCvf-yZ.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-D3VI8UYv.js → ruby-ChLBY_iR.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-DxRf8ZAR.js → sas-cwn4x2VY.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-DRRA6d6A.js → scss-jSrJlzLI.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CoOBe3_y.js → shellscript-D5nAd0gs.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-DOV1HYxf.js → shellsession-BZY4-Tdh.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-Acpmpp1W.js → soy-COppPOKr.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-DeT9N-fA.js → sql-Blc_6dOA.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DPXVxhL5.js → sseReplay-DXC7kGbN.js} +3 -3
- package/dist/assets/web/dist/assets/{stata-CbxXAMO4.js → stata-DRv3OY-h.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-BdKbi4hc.js → surrealql-qMv_eeJ6.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-tAcjDM7R.js → svelte-d1Edb6Rz.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CObINNJj.js → templ-CUMSIOXG.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BYwpZT1J.js → tex-BGZQA-Ei.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-BciREL5R.js → ts-tags-Z9ulxVsq.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-CRiv3Pr3.js → tsx-oI4bI3Vf.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-DuZZ1Ej0.js → twig-D_RXwBIX.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-jkHz_5HZ.js → typescript-BgqQDYoN.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-DOac4Gal.js → typst-5MW-KRDd.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CgOX5wXA.js → vue-CZhHHsPm.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Duu99VNw.js → vue-html-DlYy2d8v.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-Bmj_wvRJ.js → vue-vine-CJDnx-qn.js} +1 -1
- package/dist/assets/web/dist/assets/wallClock-DmaC1sKA.js +1 -0
- package/dist/assets/web/dist/assets/{xml-BtUIVeWH.js → xml-BRBDk65Q.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-wX71n1E5.js → xsl-BlCykPxo.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-CdLdXvmz.js → yaml-BqW48szf.js} +1 -1
- package/dist/cli.js +2856 -1224
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-qqs_p96-.js +0 -1
- package/dist/assets/web/dist/assets/CostsPage-Dd9aXxxI.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-VLTFopnl.js +0 -2
- package/dist/assets/web/dist/assets/InputMenu-lQP3ia9D.js +0 -1
- package/dist/assets/web/dist/assets/NotFoundPage-8ViskpN3.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-D2gHef5v.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-nWLN8BOd.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-D3Q48CJW.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CHy_IfZG.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-C6kX5om4.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +0 -1
- package/dist/assets/web/dist/assets/RunsTabs-WyeG5tKb.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-B8D5AU4d.js +0 -1
- package/dist/assets/web/dist/assets/SettingSelect-UTHsqUUS.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-BoW7Zjzt.js +0 -1
- package/dist/assets/web/dist/assets/StatusDot-BGHureCJ.js +0 -1
- package/dist/assets/web/dist/assets/Tooltip-CiP8qPd7.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-2zEeRBP1.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-D51enaGR.js +0 -1
- package/dist/assets/web/dist/assets/eventSource-By0B_w4S.js +0 -1
- package/dist/assets/web/dist/assets/favicon-D1hkyC5j.js +0 -1
- package/dist/assets/web/dist/assets/main-ClluP-Qf.js +0 -28
- package/dist/assets/web/dist/assets/main-CpkwzHX6.css +0 -1
- package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +0 -1
- package/dist/assets/web/dist/assets/wallClock-DD_fPuSY.js +0 -1
|
@@ -247,14 +247,146 @@ export function formatDisposition(d: FindingDisposition): string {
|
|
|
247
247
|
return `${d.findingId}: ${d.disposition}${d.note ? ` — ${d.note}` : ""}`;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/** Where the posted body's file links point: the PR's repository and the
|
|
251
|
+
* head the review is pinned to (the post-step's `commitId`). */
|
|
252
|
+
export interface ReviewBodyTarget {
|
|
253
|
+
/** `owner/name` */
|
|
254
|
+
repo: string;
|
|
255
|
+
/** The pinned head, full sha. */
|
|
256
|
+
head: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The verdict word the callout carries, per verdict kind (or its absence). */
|
|
260
|
+
function verdictWord(verdict: ReviewVerdict | undefined): string {
|
|
261
|
+
if (!verdict) return "No verdict";
|
|
262
|
+
return verdict.verdict === "approve" ? "Approved" : "Changes requested";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** GitHub's alert callout kind per verdict: an approve is a note, changes
|
|
266
|
+
* requested a warning, a missing verdict a caution. */
|
|
267
|
+
function calloutKind(verdict: ReviewVerdict | undefined): "NOTE" | "WARNING" | "CAUTION" {
|
|
268
|
+
if (!verdict) return "CAUTION";
|
|
269
|
+
return verdict.verdict === "approve" ? "NOTE" : "WARNING";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** `2 findings: 1 minor, 1 nit` — counted on the ladder, most severe first;
|
|
273
|
+
* an empty array is `no findings`, no array `findings not itemized`. */
|
|
274
|
+
function findingCounts(findings: readonly Finding[] | undefined): string {
|
|
275
|
+
if (findings === undefined) return "findings not itemized";
|
|
276
|
+
if (findings.length === 0) return "no findings";
|
|
277
|
+
const by = FINDING_SEVERITIES.map((sev) => [sev, findings.filter((f) => f.severity === sev).length] as const)
|
|
278
|
+
.filter(([, n]) => n > 0)
|
|
279
|
+
.map(([sev, n]) => `${n} ${sev}`)
|
|
280
|
+
.join(", ");
|
|
281
|
+
return `${findings.length} finding${findings.length === 1 ? "" : "s"}: ${by}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** A table cell: pipes escaped so the row holds, already one line. */
|
|
285
|
+
const cell = (text: string): string => oneLine(text).replace(/\|/g, "\\|");
|
|
286
|
+
|
|
287
|
+
/** Whether a finding's `file` is a path a blob URL can point at: no
|
|
288
|
+
* whitespace, no backtick, not a URL. */
|
|
289
|
+
const isRepoPath = (file: string): boolean => /^[^\s`]+$/.test(file) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(file);
|
|
290
|
+
|
|
291
|
+
/** The `Where` cell: the location in code, linked at the pinned head when the
|
|
292
|
+
* file is a path and a target is known. */
|
|
293
|
+
function whereCell(f: Finding, target: ReviewBodyTarget | undefined): string {
|
|
294
|
+
const location = f.line !== undefined ? `${f.file}:${f.line}` : f.file;
|
|
295
|
+
const label = `\`${cell(location)}\``;
|
|
296
|
+
if (!target || !isRepoPath(f.file)) return label;
|
|
297
|
+
const path = encodeURI(f.file).replace(/#/g, "%23").replace(/\?/g, "%3F");
|
|
298
|
+
const url = `https://github.com/${target.repo}/blob/${target.head}/${path}${f.line !== undefined ? `#L${f.line}` : ""}`;
|
|
299
|
+
return `[${label}](${url})`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The machine-readable marker the body ends with — the verdict, the head and
|
|
303
|
+
* the findings index as JSON inside an HTML comment, for a scanner that would
|
|
304
|
+
* otherwise parse the token line. `-->` can never occur inside it. */
|
|
305
|
+
function verdictMarker(verdict: ReviewVerdict | undefined, target: ReviewBodyTarget | undefined): string {
|
|
306
|
+
const head = target?.head ?? verdict?.head;
|
|
307
|
+
const payload = {
|
|
308
|
+
verdict: verdict?.verdict ?? "none",
|
|
309
|
+
...(head ? { head } : {}),
|
|
310
|
+
...(verdict?.findings
|
|
311
|
+
? {
|
|
312
|
+
findings: verdict.findings.map((f) => ({
|
|
313
|
+
id: f.id,
|
|
314
|
+
severity: f.severity,
|
|
315
|
+
file: f.file,
|
|
316
|
+
...(f.line !== undefined ? { line: f.line } : {}),
|
|
317
|
+
})),
|
|
318
|
+
}
|
|
319
|
+
: {}),
|
|
320
|
+
};
|
|
321
|
+
return `<!-- switchboard:verdict ${JSON.stringify(payload).replace(/-->/g, "--\\u003e")} -->`;
|
|
322
|
+
}
|
|
323
|
+
|
|
250
324
|
/**
|
|
251
|
-
* Build the body posted to GitHub
|
|
252
|
-
*
|
|
253
|
-
*
|
|
325
|
+
* Build the body posted to GitHub, rendered from the typed verdict
|
|
326
|
+
* (docs/reference/specs/agent-review.md item 5b): the deterministic verdict
|
|
327
|
+
* line first (the auto-approve contract — never `LGTM:` unless the verdict is
|
|
328
|
+
* `approve`), a GitHub alert callout with the verdict word, the pinned head
|
|
329
|
+
* and the finding counts, the findings as a table (severity, id + title, the
|
|
330
|
+
* file linked at the head), the model's text folded under `Full review`, and
|
|
331
|
+
* the machine-readable marker last. The prose decides nothing above the fold.
|
|
254
332
|
*/
|
|
255
|
-
export function buildReviewPostBody(
|
|
256
|
-
|
|
257
|
-
|
|
333
|
+
export function buildReviewPostBody(
|
|
334
|
+
answer: string,
|
|
335
|
+
verdict: ReviewVerdict | undefined,
|
|
336
|
+
target?: ReviewBodyTarget,
|
|
337
|
+
): string {
|
|
338
|
+
const facts = [
|
|
339
|
+
`**${verdictWord(verdict)}**`,
|
|
340
|
+
...(target ? [`head \`${target.head.slice(0, 7)}\``] : []),
|
|
341
|
+
verdict ? findingCounts(verdict.findings) : "the run ended without a submit_verdict call",
|
|
342
|
+
];
|
|
343
|
+
const parts: string[] = [verdictLine(verdict), `> [!${calloutKind(verdict)}]\n> ${facts.join(" · ")}`];
|
|
344
|
+
const findings = verdict?.findings ?? [];
|
|
345
|
+
if (findings.length > 0) {
|
|
346
|
+
parts.push(
|
|
347
|
+
[
|
|
348
|
+
"| Severity | Finding | Where |",
|
|
349
|
+
"| --- | --- | --- |",
|
|
350
|
+
...findings.map((f) => `| ${f.severity} | **${cell(f.id)}** ${cell(f.title)} | ${whereCell(f, target)} |`),
|
|
351
|
+
].join("\n"),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const prose = answer.trim();
|
|
355
|
+
if (prose) parts.push(`<details>\n<summary>Full review</summary>\n\n${prose}\n\n</details>`);
|
|
356
|
+
parts.push(verdictMarker(verdict, target));
|
|
357
|
+
return parts.join("\n\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The channel reply for a review run, rendered from the typed verdict
|
|
362
|
+
* (agent-review.md item 5b): the verdict line, one bullet per finding, then
|
|
363
|
+
* where the review was posted and the run link. The model's write-up rides
|
|
364
|
+
* along only when it landed nowhere else — no GitHub post (Slack-only, an
|
|
365
|
+
* opt-out, a guard refusal) or findings not itemized (a list that says
|
|
366
|
+
* nothing is no substitute) — so the review's text is always somewhere a
|
|
367
|
+
* person reads it. No verdict → the bare answer with the link, as before.
|
|
368
|
+
*/
|
|
369
|
+
export function buildReviewChannelReply(input: {
|
|
370
|
+
answer: string;
|
|
371
|
+
verdict: ReviewVerdict | undefined;
|
|
372
|
+
/** The PR the post landed on, or undefined when nothing was posted. */
|
|
373
|
+
posted: { repo: string; number: number } | undefined;
|
|
374
|
+
liveUrl: string | undefined;
|
|
375
|
+
}): string {
|
|
376
|
+
const { answer, verdict, posted, liveUrl } = input;
|
|
377
|
+
const tail = [
|
|
378
|
+
...(posted && verdict ? [`Posted to ${posted.repo}#${posted.number}`] : []),
|
|
379
|
+
...(liveUrl ? [`[Live run](${liveUrl})`] : []),
|
|
380
|
+
].join(" · ");
|
|
381
|
+
const blocks: string[] = [];
|
|
382
|
+
if (!verdict) blocks.push(answer);
|
|
383
|
+
else {
|
|
384
|
+
blocks.push([verdictLine(verdict), ...(verdict.findings ?? []).map((f) => `- ${formatFinding(f)}`)].join("\n"));
|
|
385
|
+
const compact = posted !== undefined && verdict.findings !== undefined;
|
|
386
|
+
if (!compact && answer.trim()) blocks.push(answer.trim());
|
|
387
|
+
}
|
|
388
|
+
if (tail) blocks.push(tail);
|
|
389
|
+
return blocks.join("\n\n");
|
|
258
390
|
}
|
|
259
391
|
|
|
260
392
|
// --- Dispositions (the coding side's answer to findings) -------------------
|
|
@@ -94,6 +94,10 @@ export type RunNoteKind =
|
|
|
94
94
|
| "sandbox_restarted"
|
|
95
95
|
| "stop_requested"
|
|
96
96
|
| "stopped"
|
|
97
|
+
/** A stop the loop or turn had to ask pi for again reached it: the series a
|
|
98
|
+
* failed abort's write opened (its `harness_error` line) closes here with
|
|
99
|
+
* the count of re-asks (harness-pi item 16). Written by the pi harness. */
|
|
100
|
+
| "stop_landed"
|
|
97
101
|
/** Setup spans the request's stream sink had to drop before this run was
|
|
98
102
|
* bound (docs/reference/specs/tracing.md): `summary` says how many, `from`/`to` the
|
|
99
103
|
* interval, which the partition reports as not recorded. */
|
|
@@ -142,6 +146,16 @@ export type RunNoteKind =
|
|
|
142
146
|
* after the attach, before the first turn, so the run page explains a
|
|
143
147
|
* sandbox run that shows resident steps. */
|
|
144
148
|
| "cold_sandbox"
|
|
149
|
+
/** The run ledger would not track this run (docs/reference/specs/run-history.md
|
|
150
|
+
* item 54): its reservation met the row of a run this process was closing
|
|
151
|
+
* (a restart from its request, or the fresh turn for its follow-ups), waited
|
|
152
|
+
* for that finish, and the row still stood — the finish failed or was
|
|
153
|
+
* refused — so no handoff, resume or reclaim reaches this run, and its
|
|
154
|
+
* record reaches the store when it finishes. The summary names that run and
|
|
155
|
+
* how its finish ended; published by the dispatcher right after the
|
|
156
|
+
* reservation, before the attach, and carried on the card's label — the bot
|
|
157
|
+
* log's warning is not the only witness. */
|
|
158
|
+
| "ledger_untracked"
|
|
145
159
|
/** The resident kept this thread's binding where it was instead of moving
|
|
146
160
|
* it onto the branch the thread's own run opened a pull request on
|
|
147
161
|
* (docs/reference/specs/resident-repos.md item 16): the summary names the
|
|
@@ -248,6 +262,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
248
262
|
"sandbox_restarted",
|
|
249
263
|
"stop_requested",
|
|
250
264
|
"stopped",
|
|
265
|
+
"stop_landed",
|
|
251
266
|
"spans_dropped",
|
|
252
267
|
"head_moved",
|
|
253
268
|
"run_failed",
|
|
@@ -258,6 +273,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
258
273
|
"description_turn",
|
|
259
274
|
"verdict_turn",
|
|
260
275
|
"cold_sandbox",
|
|
276
|
+
"ledger_untracked",
|
|
261
277
|
"rebind_refused",
|
|
262
278
|
"work_left_behind",
|
|
263
279
|
"workspace_torn_down",
|
|
@@ -373,6 +389,7 @@ export function isHeadMaterial(event: RunEvent): boolean {
|
|
|
373
389
|
event.kind === "mcp_unavailable" ||
|
|
374
390
|
event.kind === "spans_dropped" ||
|
|
375
391
|
event.kind === "cold_sandbox" ||
|
|
392
|
+
event.kind === "ledger_untracked" ||
|
|
376
393
|
event.kind === "rebind_refused"
|
|
377
394
|
);
|
|
378
395
|
case "span_start":
|
|
@@ -445,7 +462,7 @@ export type RouteInputValue = RouteInputLeafOrList | RouteInputObject3;
|
|
|
445
462
|
* `pasted` — the typed line that followed a hand-back in its thread, with the
|
|
446
463
|
* same receipt, ran. A routed read carries no outcome: it is not a decision
|
|
447
464
|
* about a state change. */
|
|
448
|
-
export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "pasted";
|
|
465
|
+
export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "pasted" | "refused";
|
|
449
466
|
|
|
450
467
|
export type RunEvent =
|
|
451
468
|
/** `callId` is the provider's tool_use id — the explicit pair key between a
|
|
@@ -794,7 +811,18 @@ export type RunEvent =
|
|
|
794
811
|
* round and its fix round share an index. Published by the ship pipeline
|
|
795
812
|
* straight to the registry (like `pr_opened`), never through the runner.
|
|
796
813
|
* Additive: unknown → ignored. */
|
|
797
|
-
| {
|
|
814
|
+
| {
|
|
815
|
+
type: "ship_round";
|
|
816
|
+
index: number;
|
|
817
|
+
agent: string;
|
|
818
|
+
outcome: ShipRoundOutcome;
|
|
819
|
+
/** The severity gate fired on this approve (agent-ship item 9): the level
|
|
820
|
+
* in force and the gated findings as `id (severity)` — a mismatch the
|
|
821
|
+
* child's own parser should have made impossible, kept visible. */
|
|
822
|
+
gate?: { level: string; findings: string[] };
|
|
823
|
+
seq?: number;
|
|
824
|
+
at?: number;
|
|
825
|
+
}
|
|
798
826
|
/** The request router's decision (docs/reference/specs/routing-and-config.md
|
|
799
827
|
* item 21): the preset a plain message was routed to, the one-line reason
|
|
800
828
|
* the router gave (redacted, capped — the same text the card's `routed:`
|
|
@@ -836,6 +864,10 @@ export type RunEvent =
|
|
|
836
864
|
input?: { readonly [key: string]: RouteInputValue };
|
|
837
865
|
receipt?: string;
|
|
838
866
|
outcome?: RouteOutcome;
|
|
867
|
+
/** The refusal's code (src/core/refusal.ts) when `outcome` is `refused`
|
|
868
|
+
* (record 0054): a refusal after a command was bound is a run
|
|
869
|
+
* record, and the door report counts it by cause and code. */
|
|
870
|
+
refusalCode?: string;
|
|
839
871
|
handBackRunId?: string;
|
|
840
872
|
seq?: number;
|
|
841
873
|
at?: number;
|
|
@@ -291,6 +291,16 @@ export function pushedBranchesOf(events: readonly RunEvent[]): PushedBranch[] {
|
|
|
291
291
|
return [...byRef].map(([ref, pr]) => ({ ref, pr }));
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/** A call a run's ending may have left running in its workspace (harness.md
|
|
295
|
+
* item 13): the id the record pairs by, the tool, and the line the
|
|
296
|
+
* `workspace_torn_down` note names it by — the `tool_call`'s own summary
|
|
297
|
+
* while the record holds it, else the tool and the id. */
|
|
298
|
+
export interface CallInFlight {
|
|
299
|
+
callId: string;
|
|
300
|
+
tool: string;
|
|
301
|
+
summary: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
294
304
|
/** The tool calls a run's ending may have left running in its workspace, in
|
|
295
305
|
* the record's order — what the workspace's release reads to tear the
|
|
296
306
|
* workspace down rather than wait for an idle the command would refuse, and
|
|
@@ -299,26 +309,38 @@ export function pushedBranchesOf(events: readonly RunEvent[]): PushedBranch[] {
|
|
|
299
309
|
* settled, whatever the run's status, and stays cut whatever lands for it
|
|
300
310
|
* later — an unmarked result (a post-turn's bridge reads the earlier
|
|
301
311
|
* execution's aborted settle as its own), or the call announced again under
|
|
302
|
-
* the same id; a call with no result at
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
>();
|
|
312
|
+
* the same id; a call with no result at all was left open by a run that
|
|
313
|
+
* failed, was interrupted or hard-stopped, while on a run that completed or
|
|
314
|
+
* stopped softly it is a relayed tool that ran in the bot or a result lost to
|
|
315
|
+
* a transport gap, nothing running. Calls and results pair by id. The events
|
|
316
|
+
* are the registry's bounded backlog (`runRegistry/backlog.ts` drops the
|
|
317
|
+
* oldest past its count or byte bound), so on a long run the line of a
|
|
318
|
+
* command that hung early may be gone while its cut result — the ending's,
|
|
319
|
+
* landing last — survives: a cut result for a call the record no longer
|
|
320
|
+
* holds is the harness's own word that the call was ended and not settled,
|
|
321
|
+
* and counts by itself, named by its tool and id; an unmarked result for a
|
|
322
|
+
* call never opened counts for nothing. */
|
|
323
|
+
export function callsInFlight(events: readonly RunEvent[], status: RunStatus): CallInFlight[] {
|
|
324
|
+
const calls = new Map<string, { call: CallInFlight; state: "open" | "cut" | "settled" }>();
|
|
316
325
|
for (const e of events) {
|
|
317
326
|
if (e.type === "tool_call" && e.callId !== undefined) {
|
|
318
|
-
|
|
327
|
+
const seen = calls.get(e.callId);
|
|
328
|
+
if (seen?.state === "cut") {
|
|
329
|
+
// Announced again after its cut — or after an orphan cut, the line the
|
|
330
|
+
// backlog evicted now written again: the line is the record's, the
|
|
331
|
+
// state stays the cut's.
|
|
332
|
+
seen.call.tool = e.tool;
|
|
333
|
+
seen.call.summary = e.summary;
|
|
334
|
+
} else calls.set(e.callId, { call: { callId: e.callId, tool: e.tool, summary: e.summary }, state: "open" });
|
|
319
335
|
} else if (e.type === "tool_result" && e.callId !== undefined) {
|
|
320
336
|
const seen = calls.get(e.callId);
|
|
321
|
-
if (seen
|
|
337
|
+
if (seen === undefined) {
|
|
338
|
+
if (e.cut === true)
|
|
339
|
+
calls.set(e.callId, {
|
|
340
|
+
call: { callId: e.callId, tool: e.tool, summary: `${e.tool} (call ${e.callId})` },
|
|
341
|
+
state: "cut",
|
|
342
|
+
});
|
|
343
|
+
} else if (seen.state !== "cut") seen.state = e.cut === true ? "cut" : "settled";
|
|
322
344
|
}
|
|
323
345
|
}
|
|
324
346
|
const orderly = status === "completed" || status === "stopped_soft";
|
|
@@ -129,6 +129,13 @@ export const CONTRACT_SECTION_HEADINGS = {
|
|
|
129
129
|
* repository's rules file (~15k characters) and a long unit both inside it. */
|
|
130
130
|
export const DEFAULT_CONTRACT_MAX_CHARS = 48_000;
|
|
131
131
|
|
|
132
|
+
/** The most of a plan a reader hands the parsers: ten times the tool clip
|
|
133
|
+
* (`MAX_FILE_CHARS`, `src/execution/githubApi.ts`), which a plan that grows by
|
|
134
|
+
* amendment passes — the program plan crossed 400k characters and the seed
|
|
135
|
+
* then saw its first 25 units alone. A plan past this bound is refused by
|
|
136
|
+
* name, never parsed short. */
|
|
137
|
+
export const PLAN_MAX_CHARS = 2_000_000;
|
|
138
|
+
|
|
132
139
|
const UNIT_HEADING = /^### (U\d+)\. (.+?)\s*$/;
|
|
133
140
|
const ANY_HEADING = /^#{2,3} /;
|
|
134
141
|
const BULLET = /^- \*\*([^*]+)\*\*:(?:\s?(.*))?$/;
|
|
@@ -600,8 +600,22 @@ export type UnitEnding =
|
|
|
600
600
|
/** What a transition tells the driver beyond the next action: a round boundary
|
|
601
601
|
* the card draws (`shipRoundHeader`) and the run stream records, and the end. */
|
|
602
602
|
export type CoordinatorNote =
|
|
603
|
-
| {
|
|
603
|
+
| {
|
|
604
|
+
type: "round";
|
|
605
|
+
index: number;
|
|
606
|
+
agent: ChildPreset;
|
|
607
|
+
outcome: ShipRoundOutcome;
|
|
608
|
+
/** The severity gate fired on this approve (agent-ship item 9): the
|
|
609
|
+
* findings at or above the level in force, as `id (severity)`. The
|
|
610
|
+
* child's own verdict parser holds an approve to the same level
|
|
611
|
+
* (agent-review item 5a), so this should never be set — when it is, the
|
|
612
|
+
* verdict was parsed at another level (a lost directive, an older
|
|
613
|
+
* record, a harness around `submit_verdict`) and the row, the card and
|
|
614
|
+
* the log say so instead of routing silently into the findings step. */
|
|
615
|
+
gate?: { level: AddressSeverity; findings: string[] };
|
|
616
|
+
}
|
|
604
617
|
| { type: "ended"; ending: UnitEnding };
|
|
618
|
+
type RoundNote = Extract<CoordinatorNote, { type: "round" }>;
|
|
605
619
|
|
|
606
620
|
/** How the pipeline's budget went, in ms: the coding rounds' (round 0 and the
|
|
607
621
|
* findings steps), the review rounds', and everything else (branching,
|
|
@@ -740,7 +754,7 @@ export const WAIT_MARGIN_MS = SHIP_WAIT.marginMinutes * MIN;
|
|
|
740
754
|
* one chunk of the runner's time, not the child's whole budget. Five minutes
|
|
741
755
|
* is the machine's one cadence for asking the bot what it cannot be told (the
|
|
742
756
|
* margin and the merge poll are the same number), and it keeps a round to a
|
|
743
|
-
* few steps: a coding child's
|
|
757
|
+
* few steps: a coding child's 90 minutes are eighteen waits and eighteen reads. */
|
|
744
758
|
export const WAIT_CHUNK_MS = SHIP_WAIT.chunkMinutes * MIN;
|
|
745
759
|
/** The merge wait is a round of its own: its minutes are carved from the
|
|
746
760
|
* pipeline's remainder when the door is first asked (the merge wait's ask and
|
|
@@ -939,7 +953,10 @@ function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[]
|
|
|
939
953
|
return { state: { ...s, phase: ENDED, ending }, notes: [...notes, { type: "ended", ending }] };
|
|
940
954
|
}
|
|
941
955
|
|
|
942
|
-
|
|
956
|
+
/** A gated finding as the gate note names it: `F1 (minor)`. */
|
|
957
|
+
const gateLabel = (f: Finding): string => `${f.id} (${f.severity})`;
|
|
958
|
+
|
|
959
|
+
const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): RoundNote => ({
|
|
943
960
|
type: "round",
|
|
944
961
|
index: round.index,
|
|
945
962
|
agent: presetOf(round.kind),
|
|
@@ -1114,6 +1131,10 @@ function settleReview(
|
|
|
1114
1131
|
const level = next.input.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
|
|
1115
1132
|
const gated = findingsAtOrAbove(verdict.findings, level);
|
|
1116
1133
|
if (gated.length > 0) {
|
|
1134
|
+
// The gate fired — which the child's parser should have made impossible
|
|
1135
|
+
// (agent-review item 5a): the round note carries what it caught, so the
|
|
1136
|
+
// mismatch is seen and not just routed around.
|
|
1137
|
+
notes[0] = { ...roundNote(round, verdict.verdict), gate: { level, findings: gated.map(gateLabel) } };
|
|
1117
1138
|
if (mode !== undefined)
|
|
1118
1139
|
return end(
|
|
1119
1140
|
next,
|
|
@@ -24,8 +24,12 @@ export interface ProgressInput {
|
|
|
24
24
|
/** The heads the segment's coding run pushed (`RunRecord.pushed`). */
|
|
25
25
|
pushed: readonly PushedHeadFact[];
|
|
26
26
|
/** The head the segment started from — the previous segment's recorded sha.
|
|
27
|
-
* Absent on a fresh branch, where
|
|
27
|
+
* Absent on a fresh branch, where the base head stands in for it. */
|
|
28
28
|
startHead?: string;
|
|
29
|
+
/** The base branch's head the unit's branch was created at. On a fresh
|
|
30
|
+
* branch (no `startHead`) a push of this head is the branch's creation,
|
|
31
|
+
* not the unit's progress; with neither known, any push is progress. */
|
|
32
|
+
baseHead?: string;
|
|
29
33
|
/** When the segment's lease began; a push stamped before it is not this segment's. */
|
|
30
34
|
leaseStartedAt?: number;
|
|
31
35
|
/** The previous segment's handoff and this one's, when both exist. */
|
|
@@ -53,7 +57,10 @@ export function progressOf(input: ProgressInput): Progress {
|
|
|
53
57
|
if (last !== undefined) {
|
|
54
58
|
const newerThanLease =
|
|
55
59
|
input.leaseStartedAt === undefined || last.at === undefined || last.at >= input.leaseStartedAt;
|
|
56
|
-
|
|
60
|
+
// A fresh branch starts at the base head, so that head is the bar a push
|
|
61
|
+
// must move past when no segment recorded a start.
|
|
62
|
+
const startHead = input.startHead ?? input.baseHead;
|
|
63
|
+
const moved = startHead === undefined || !sameSha(last.sha, startHead);
|
|
57
64
|
if (newerThanLease && moved) return { progressed: true, by: "push", sha: last.sha };
|
|
58
65
|
}
|
|
59
66
|
const pushWhy = `no head newer than the lease's start was pushed to \`${input.branch}\``;
|
|
@@ -95,7 +102,7 @@ export function renewalDecision(input: RenewalInput): RenewalDecision {
|
|
|
95
102
|
const detail =
|
|
96
103
|
input.grant.renewals === 0
|
|
97
104
|
? "the grant holds no renewals"
|
|
98
|
-
: `the grant's ${input.grant.renewals} renewal${input.grant.renewals === 1 ? "" : "s"}
|
|
105
|
+
: `the grant's ${input.grant.renewals} renewal${input.grant.renewals === 1 ? " is" : "s are"} spent`;
|
|
99
106
|
return { renew: false, why: "grant_exhausted", detail, renewalsLeft };
|
|
100
107
|
}
|
|
101
108
|
const cap = input.grant.costCapUsd;
|
|
@@ -26,6 +26,11 @@ export interface AttrDomain {
|
|
|
26
26
|
requester: "message" | "relay-footer" | "thread-parent" | "bot";
|
|
27
27
|
// dispatch.* / run.* / post.*
|
|
28
28
|
outcome: string;
|
|
29
|
+
/** The refusal's code (src/core/refusal.ts) — on the `dispatch.refuse` span
|
|
30
|
+
* and the request's root, so refusals are countable from the trace alone. */
|
|
31
|
+
refusal: string;
|
|
32
|
+
/** The refusal's cause, from the one code→cause table in src/core/refusal.ts. */
|
|
33
|
+
cause: "request" | "policy" | "system";
|
|
29
34
|
count: number;
|
|
30
35
|
backend: Backend;
|
|
31
36
|
// run.command
|
|
@@ -114,6 +119,8 @@ export type SpanAttrs = { readonly [K in SpanAttrKey]?: AttrDomain[K] };
|
|
|
114
119
|
const IDENTIFIER_KEYS: ReadonlySet<SpanAttrKey> = new Set<SpanAttrKey>([
|
|
115
120
|
"runId",
|
|
116
121
|
"outcome",
|
|
122
|
+
"refusal",
|
|
123
|
+
"cause",
|
|
117
124
|
"command",
|
|
118
125
|
"route",
|
|
119
126
|
"host",
|
|
@@ -163,6 +170,8 @@ const ATTR_TYPE: Record<SpanAttrKey, "string" | "number" | "boolean"> = {
|
|
|
163
170
|
dedupe: "string",
|
|
164
171
|
requester: "string",
|
|
165
172
|
outcome: "string",
|
|
173
|
+
refusal: "string",
|
|
174
|
+
cause: "string",
|
|
166
175
|
count: "number",
|
|
167
176
|
backend: "string",
|
|
168
177
|
command: "string",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Deliberately free of node: imports so wrangler can bundle it into Workers.
|
|
5
5
|
// The numbers are rows of src/core/budgets.ts (docs/decisions/0046); this file
|
|
6
6
|
// keeps their names for the readers that learned them here.
|
|
7
|
-
import { ALLOWANCES, BASH_COMMAND, MINUTE_MS } from "../core/budgets.js";
|
|
7
|
+
import { ALLOWANCES, ATTACH_REQUEST_MIN_MS, BASH_COMMAND, MINUTE_MS } from "../core/budgets.js";
|
|
8
8
|
|
|
9
9
|
/** Default per-command budget when the caller passes no timeoutMs. */
|
|
10
10
|
export const BASH_TIMEOUT_MS = BASH_COMMAND.defaultMinutes * MINUTE_MS;
|
|
@@ -50,8 +50,10 @@ export function clampBashTimeout(requested: unknown): number {
|
|
|
50
50
|
export type RunBudget =
|
|
51
51
|
{ kind: "unchanged" } | { kind: "clipped"; timeoutMs: number; note: string } | { kind: "exhausted"; note: string };
|
|
52
52
|
|
|
53
|
+
/** Whole seconds for the notes below, never negative: the two clip notes phrase the reserve alike. */
|
|
54
|
+
const secs = (ms: number): number => Math.max(0, Math.round(ms / 1000));
|
|
55
|
+
|
|
53
56
|
export function bashBudgetWithinRun(wantedMs: number, remainingMs: number): RunBudget {
|
|
54
|
-
const secs = (ms: number) => Math.max(0, Math.round(ms / 1000));
|
|
55
57
|
const cap = Math.trunc(remainingMs - RUN_DEADLINE_RESERVE_MS);
|
|
56
58
|
if (cap < BASH_TIMEOUT_MIN_MS) {
|
|
57
59
|
return {
|
|
@@ -69,6 +71,48 @@ export function bashBudgetWithinRun(wantedMs: number, remainingMs: number): RunB
|
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/** The bound on an attach request an executor opens while its run has
|
|
75
|
+
* `remainingMs` of wall clock left (`ResidentExecutorOptions.remainingMs`):
|
|
76
|
+
* `bounded` by the attach's own default, `BASH_TIMEOUT_MS` — an attach may
|
|
77
|
+
* clone and install deps — clipped to what the run has left less the same
|
|
78
|
+
* write-up reserve a command keeps back, so an attach never holds a run past
|
|
79
|
+
* its lease and the model keeps its write-up; never raised above the default.
|
|
80
|
+
* Inside the reserve, or with less than an attach's floor past it
|
|
81
|
+
* (`ATTACH_REQUEST_MIN_MS`: a bound too short to clone would be cut and
|
|
82
|
+
* struck as a rollout), `exhausted`: the request is not opened at all — the
|
|
83
|
+
* resident would run the attach to its end server-side for a run that is
|
|
84
|
+
* ending, and the caller would get a deadline it could read as waitable — and
|
|
85
|
+
* the note names the run's clock and the bound that refused (the reserve, or
|
|
86
|
+
* the floor past it): it is the one sentence every reader carries, the
|
|
87
|
+
* executor's refusal and the relaunch's note alike. The command's own budget
|
|
88
|
+
* never bounds an attach: the attach is not the command, and the command's
|
|
89
|
+
* one-second floor is the model's to choose. */
|
|
90
|
+
export type AttachBound = { kind: "bounded"; timeoutMs: number } | { kind: "exhausted"; note: string };
|
|
91
|
+
|
|
92
|
+
export function attachBoundWithinRun(remainingMs: number): AttachBound {
|
|
93
|
+
const left = Math.trunc(remainingMs - RUN_DEADLINE_RESERVE_MS);
|
|
94
|
+
// The one sentence every reader of an exhausted bound carries — the
|
|
95
|
+
// executor's refusal, the relaunch's note — worded by the bound that refused.
|
|
96
|
+
if (left <= 0) {
|
|
97
|
+
return {
|
|
98
|
+
kind: "exhausted",
|
|
99
|
+
note:
|
|
100
|
+
`the run has ${secs(remainingMs)}s of wall clock left, inside the ${secs(RUN_DEADLINE_RESERVE_MS)}s ` +
|
|
101
|
+
"write-up reserve, so no attach was opened",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (left < ATTACH_REQUEST_MIN_MS) {
|
|
105
|
+
return {
|
|
106
|
+
kind: "exhausted",
|
|
107
|
+
note:
|
|
108
|
+
`the run has ${secs(remainingMs)}s of wall clock left, only ${secs(left)}s past the ` +
|
|
109
|
+
`${secs(RUN_DEADLINE_RESERVE_MS)}s write-up reserve — under the ${secs(ATTACH_REQUEST_MIN_MS)}s an attach needs — ` +
|
|
110
|
+
"so no attach was opened",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { kind: "bounded", timeoutMs: Math.min(BASH_TIMEOUT_MS, left) };
|
|
114
|
+
}
|
|
115
|
+
|
|
72
116
|
/** The line a timed-out command shows the model: names the limit that fired
|
|
73
117
|
* and the knob that raises it, so the model can self-correct (re-run with a
|
|
74
118
|
* larger timeoutMs, split the command, or background it) instead of guessing
|
|
@@ -180,11 +180,15 @@ export function killStaleBuildProcessesCommand(user: string, dir: string): strin
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/** Signature of a step killed from OUTSIDE its own budget: the exit status of
|
|
183
|
-
* SIGTERM (128 + 15)
|
|
184
|
-
* a
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
|
|
183
|
+
* SIGTERM (128 + 15) — as a shell reports it (`exit 143`) or as the platform
|
|
184
|
+
* words a container that died under a short exec ("Container exited with
|
|
185
|
+
* unexpected exit code: 143"; seen live on the wake path's ready-stamp probe
|
|
186
|
+
* when a stop landed on it) — bash's "Session terminated" on a killed login
|
|
187
|
+
* shell, or a tool naming the signal. Only 143: the platform's same wording
|
|
188
|
+
* with any other code (1, 137, 139) is a genuine crash and stays a failure.
|
|
189
|
+
* A bare "killed" is NOT enough — compilers and OOM messages say it too —
|
|
190
|
+
* and a step the cycle itself timed out is a real failure however it died. */
|
|
191
|
+
const INTERRUPTION_SIGNATURE = /\bexit 143\b|exit code: 143\b|Session terminated|SIGTERM|^restore-interrupted:/;
|
|
188
192
|
|
|
189
193
|
/** The wordings git gives when the MIRROR itself is broken: its `origin`
|
|
190
194
|
* remote gone from `config`, the repository not a repository, an object store
|
|
@@ -154,10 +154,15 @@ export const SANDBOX_STARTING_EXPLANATION =
|
|
|
154
154
|
"the thread's sandbox container is starting (image pull, boot, runtime) — nothing ran yet; the request is re-sent once it is up";
|
|
155
155
|
|
|
156
156
|
/** The executor waits at most this long for a container to start, whatever
|
|
157
|
-
* the command's own budget: the platform's own start allowances (
|
|
158
|
-
* instance, 90 s for the port) plus a slow image pull fit inside it,
|
|
159
|
-
* 60 s command is never killed by a two-minute start it did not cause.
|
|
160
|
-
|
|
157
|
+
* the command's own budget: the platform's own start allowances (the
|
|
158
|
+
* instance grant, 90 s for the port) plus a slow image pull fit inside it,
|
|
159
|
+
* and a 60 s command is never killed by a two-minute start it did not cause.
|
|
160
|
+
* Ten minutes, not five: under a midday burst the platform admitted about
|
|
161
|
+
* seven starts at once and granted the rest 2.5 to 5 minutes later — two
|
|
162
|
+
* threads of twenty-four died at the five-minute mark with their container
|
|
163
|
+
* a minute away. A run that waits ten minutes for its container starts; one
|
|
164
|
+
* that dies at five does not, and nothing else in the run was in progress. */
|
|
165
|
+
export const SANDBOX_START_WAIT_MAX_MS = 10 * 60_000;
|
|
161
166
|
|
|
162
167
|
/** Backoff between re-sends while a container starts: 5 s, 10 s, then 15 s.
|
|
163
168
|
* A start takes tens of seconds, not minutes, so the poll is denser than the
|