@coreplane/switchboard 1.235.0 → 1.237.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/Dockerfile +11 -0
- package/dist/assets/config/config.example.yaml +9 -4
- package/dist/assets/deploy/cloudflare-memory/worker.ts +12 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +18 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +290 -118
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +104 -17
- package/dist/assets/package-lock.json +212 -3
- package/dist/assets/package.json +4 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/authz/authorize.ts +25 -6
- package/dist/assets/src/core/authz/policy.ts +9 -0
- package/dist/assets/src/core/authz/types.ts +15 -1
- package/dist/assets/src/core/coordinator/contract.ts +24 -4
- package/dist/assets/src/core/coordinator/driver.ts +36 -4
- package/dist/assets/src/core/runEvents.ts +46 -7
- package/dist/assets/src/core/runFriction.ts +1 -0
- package/dist/assets/src/core/ship/contract.ts +3 -1
- package/dist/assets/src/core/ship/coordinator.ts +236 -30
- package/dist/assets/src/core/trace/types.ts +5 -0
- package/dist/assets/src/execution/residentDepsStore.ts +14 -2
- package/dist/assets/src/execution/residentDisk.ts +21 -10
- package/dist/assets/src/execution/residentDiskBudget.ts +45 -11
- package/dist/assets/src/execution/residentHead.ts +34 -8
- package/dist/assets/src/execution/residentRebind.ts +88 -31
- package/dist/assets/src/execution/sandboxErrors.ts +72 -0
- package/dist/assets/src/execution/sandboxLifecycle.ts +33 -0
- package/dist/assets/src/execution/sandboxStart.ts +90 -0
- package/dist/assets/web/dist/.vite/manifest.json +437 -423
- package/dist/assets/web/dist/assets/AppShell-MYHNxJks.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-D-v8am88.js → CostsPage-DKk9OjB0.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-CvlWP7Eq.js → DeliveryPage-Kz6js49i.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-BzWS4Ca1.js → NotFoundPage-BXLrQQzy.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-Dc23v_8v.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-TR5D1TRR.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-mkUj7WEN.js +9 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-oN9GkVEA.js → RunRoutePage-CQI17__g.js} +3 -3
- package/dist/assets/web/dist/assets/RunsIndexPage-yIP4PlKP.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-DEICQ4FY.js → RunsTabs-XpCqeWu4.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-CdCVyNbh.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DuA8JpLU.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-D14iJMy7.js → StatusDot-DmNHX6am.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-B0Ob5MQ4.js → Tooltip-qVnJTgSt.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-BcTeH7m2.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Cw130Zyi.js → angular-html-DYtV9uIN.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DY1m4C2s.js → angular-ts-DF2P25Zp.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-W6Ty7v05.js → apl-Ff96wxko.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DlLVHwVk.js → astro-LbBnZna-.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-Bco3tDh4.js → blade-DltuxeBW.js} +1 -1
- package/dist/assets/web/dist/assets/{c-1LaFY_cj.js → c-DBg7yuPx.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-B-OvrOL5.js → chapel-C83hUAds.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-Ch3EPlHu.js → cobol-DB0yV_2D.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-hZuITnto.js → coffee-Bpih6Kzl.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-B8iRVo8m.js → cpp-CU00ZeRn.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DKveI7lr.js → crystal-f8ImJkux.js} +1 -1
- package/dist/assets/web/dist/assets/{css-_2_CD-rr.js → css-4VrC4ISo.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-pOdUzj6D.js → dist-TAGawIfM.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-CYMux25J.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BenMDPct.js → edge-BwFDzfS_.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-CBbdEqG6.js → elixir-wwIYIpJm.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DMub_2lY.js → elm-DyDuM3v3.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CI9He_Up.js → erb-DFJBEVrh.js} +1 -1
- package/dist/assets/web/dist/assets/{favicon-BQsePYv5.js → favicon-CSt-qDvc.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-BBfhnyF8.js → git-rebase-CuDv-44X.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-CR9Fxrau.js → glimmer-js-ZFnrm4J3.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CwFwX_XS.js → glimmer-ts-XY4Yqshp.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Daiodp1r.js → glsl-xnR8efx0.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-DZvrsEkB.js → graphql-D-gO5tnP.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BaXOzdg7.js → hack-CSxbW4ax.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CMskq_VA.js → haml-D4ar42QT.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-Dhw_f7jD.js → handlebars-CMLbDE7o.js} +1 -1
- package/dist/assets/web/dist/assets/{html-C9k99z0z.js → html-CVekU4Fs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-VckPItXN.js → html-derivative-St3HHw_N.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXU_3h9v.js → http-Mon0DWHS.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-KU24JNdp.js → hurl-Cbl5IvBs.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-FLJu5mR9.js +1 -0
- package/dist/assets/web/dist/assets/{java-BbZjTNgf.js → java-Bprck1l-.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-DXCdqcZZ.js → javascript-C73G1-UR.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-pDlsjTVp.js → jinja-C4DuRxYJ.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DsXDk66e.js → jison-B2U03GEC.js} +1 -1
- package/dist/assets/web/dist/assets/{json-tCxXfRgG.js → json-CvTZUDzx.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-B0ZYkCmi.js → jsx-DMWKCzwo.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-BLzzeI6x.js → julia-Czn7bV_3.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DvD2_60c.js → just-BBxpNfvC.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-511zn3h7.js → latex-Bw9yg5mr.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BD0BkMox.js → liquid-BRCjaydE.js} +1 -1
- package/dist/assets/web/dist/assets/{indexFormat-B-pd4r7Y.js → localIso-CNA-bhS-.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-NFicKm6N.js → lua-DUu2KIC0.js} +1 -1
- package/dist/assets/web/dist/assets/main-csihqEj5.css +1 -0
- package/dist/assets/web/dist/assets/main-mUd_TJjH.js +28 -0
- package/dist/assets/web/dist/assets/{marko-1-G_nEXK.js → marko-CZJZIlSQ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DUe3AQLp.js → mdc-BmClq6fd.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Dg379_bA.js → nginx-Bmt3xSEm.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BnFq97ZO.js → nim-Da-i0utK.js} +1 -1
- package/dist/assets/web/dist/assets/{org-F0uiwGvq.js → org-BvAre2qP.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-B0euczkl.js → perl-v9WwFFtB.js} +1 -1
- package/dist/assets/web/dist/assets/{php-DkL3k_n7.js → php-DTU4Zyze.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-L_OjjZP4.js → pug-DhTwEqTO.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BMjB00Zz.js → qml-BUD9AC_c.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DoeLdnqR.js → r-CAnu56n7.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BzYNkAWy.js → razor-Dn0P4gAq.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CPmElMk3.js → regexp-CZyBRaVW.js} +1 -1
- package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +1 -0
- package/dist/assets/web/dist/assets/{rst-DrsjgfI7.js → rst-Ckyd5UsY.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cB42ppvx.js → ruby-BBQ62fsx.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-R5Y3NM_I.js → sas-CW35hMxG.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-C-zWxV9s.js → scss-CpV8pF4J.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-oJF96aAU.js → shellscript-CRJvsmE1.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CznECKyi.js → shellsession-CyriUZxf.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-wjHLSRag.js → soy-D5QzYUeg.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-C_BM8IOW.js → sql-CUBf_Mse.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-D31HO8aQ.js → stata-B-4n4ITz.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CKCLyplA.js → surrealql-RuWZBLtr.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-3geWk2iu.js → svelte-CO5YzCsK.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DiJr_hTD.js → templ-DbkWVaBF.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BEJGWyeF.js → tex-IuxoEcId.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-C8Mzdpxx.js → ts-tags-g6dGQPjd.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-UQiSm_p3.js → tsx-WnXwiyH2.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-_Pvzkeo9.js → twig-Lx1G7PnY.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BWMkINKK.js → typescript-BQd7biLF.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Bdg9m-e7.js → typst-D2lXPHK7.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CDZrrAZ8.js → vue-4wVQD9y9.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-tAlpNhfN.js → vue-html-DZPaRte4.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-C27UF_rh.js → vue-vine-DbmZCmu-.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-CxeDr9Zh.js → xml-DV16szyo.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bpi0uUnM.js → xsl-eo5-1sk0.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BtUgZ1GN.js → yaml-BXdX8HZf.js} +1 -1
- package/dist/cli.js +3329 -1804
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-lYVcj-k7.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DSpsye5e.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CT8fI1M3.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-Cqh8pTXC.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-sjXr8iCV.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-Tz__zLzi.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CbKCL58v.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-Bnj883ii.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-D3lG-yQ9.js +0 -28
- package/dist/assets/web/dist/assets/main-Dm11o0hc.css +0 -1
- package/dist/assets/web/dist/assets/residentDiskBudget-BMBKlYRH.js +0 -1
|
@@ -51,6 +51,29 @@ export interface ShipCaps {
|
|
|
51
51
|
* burning an attach and a model turn on a doomed round. */
|
|
52
52
|
export const SHIP_ROUND_RESERVE_MS = 3 * MIN;
|
|
53
53
|
|
|
54
|
+
/** What a round-0 coding child must leave on the pipeline's clock: two review
|
|
55
|
+
* rounds (a review and, after a fix, its re-review) and one merge poll — the
|
|
56
|
+
* loop the pipeline exists to run. The coding child's budget is clipped to
|
|
57
|
+
* the remaining wall clock MINUS this reserve (never under the two minutes a
|
|
58
|
+
* spawn accepts), so a child that uses its whole directive still hands the
|
|
59
|
+
* pipeline a clock that holds the review; without the clip a 40-minute
|
|
60
|
+
* pipeline hands 39 minutes to the child and caps out with the pull request
|
|
61
|
+
* shipped and unreviewed. */
|
|
62
|
+
export const SHIP_LOOP_RESERVE_MS = 2 * SHIP_ROUND_RESERVE_MS + 5 * MIN;
|
|
63
|
+
|
|
64
|
+
/** What a findings child (the fix after a review asked for changes) must leave
|
|
65
|
+
* on the clock: the re-review and one merge poll — one round's reserve and
|
|
66
|
+
* five minutes. Never the whole loop's: by the time a fix round runs, the
|
|
67
|
+
* first review has already happened, and holding two rounds back from a late
|
|
68
|
+
* fix would cap a pipeline that still has the time. */
|
|
69
|
+
export const SHIP_FIX_RESERVE_MS = SHIP_ROUND_RESERVE_MS + 5 * MIN;
|
|
70
|
+
|
|
71
|
+
/** The floor `validateShip` holds `ship.maxMinutes` to: the loop's reserve
|
|
72
|
+
* plus one round's — under it no coding child can both work and leave the
|
|
73
|
+
* review its time, so the config is refused at load rather than left to cap
|
|
74
|
+
* out on every unit. */
|
|
75
|
+
export const SHIP_MIN_MAX_MINUTES = (SHIP_LOOP_RESERVE_MS + SHIP_ROUND_RESERVE_MS) / MIN;
|
|
76
|
+
|
|
54
77
|
/** What a ship pipeline's thread and card say when the bot died under it (run-
|
|
55
78
|
* history item 36): the work it did stands on GitHub with nobody driving it,
|
|
56
79
|
* so the note names the PR when one was opened and the exact re-issue that
|
|
@@ -122,6 +145,14 @@ export function unitBranch(planId: string, slug: string): string {
|
|
|
122
145
|
return `plan/${planId}/${slug}`;
|
|
123
146
|
}
|
|
124
147
|
|
|
148
|
+
/** Whether `ref` is a unit branch of ship's own — `plan/<plan-id>/<unit-slug>`.
|
|
149
|
+
* A thread keeps the binding its last run opened a pull request on, so after
|
|
150
|
+
* a plan's unit it sits at that unit branch; a fresh task there must not take
|
|
151
|
+
* it as the base (the pull request would target the earlier unit). */
|
|
152
|
+
export function isUnitBranch(ref: string): boolean {
|
|
153
|
+
return /^plan\/[^/]+\/[^/]+$/.test(ref);
|
|
154
|
+
}
|
|
155
|
+
|
|
125
156
|
/** The plan and unit a head branch names, or undefined for any other branch — the merge grant's line. */
|
|
126
157
|
export function parsePlanBranch(branch: string): { planId: string; unitSlug: string } | undefined {
|
|
127
158
|
const m = PLAN_BRANCH.exec(branch);
|
|
@@ -407,7 +438,15 @@ export type CoordinatorAction =
|
|
|
407
438
|
| { type: "spawn"; step: string; preset: ChildPreset; round: RoundRef; budgetMinutes: number; brief: Brief }
|
|
408
439
|
| { type: "wait"; step: string; runId: string; timeoutMs: number }
|
|
409
440
|
| { type: "read-record"; step: string; runId: string }
|
|
410
|
-
| {
|
|
441
|
+
| {
|
|
442
|
+
type: "pr-check";
|
|
443
|
+
step: string;
|
|
444
|
+
/** Set after a coding child died: the bot opens the pull request from the
|
|
445
|
+
* pushed branch itself (title from the unit, body from this run's
|
|
446
|
+
* submitted description when the record holds one) instead of answering
|
|
447
|
+
* `none` over stranded work. */
|
|
448
|
+
recover?: { runId: string };
|
|
449
|
+
}
|
|
411
450
|
| { type: "merge"; step: string; prNumber: number; headSha: string }
|
|
412
451
|
| { type: "sleep"; step: string; ms: number }
|
|
413
452
|
| { type: "end"; step: string; ending: UnitEnding };
|
|
@@ -442,7 +481,14 @@ export type ChildFacts =
|
|
|
442
481
|
/** What heads the unit's branch on GitHub: nothing, an open pull request, or —
|
|
443
482
|
* with no open one — a merged one, `sha` the merge commit on the base. */
|
|
444
483
|
export type PrCheck =
|
|
445
|
-
| {
|
|
484
|
+
| {
|
|
485
|
+
state: "none";
|
|
486
|
+
/** After a recover pr-check (a dead coding child): why nothing was
|
|
487
|
+
* recovered — `no_commits` (GitHub refused the create: nothing between
|
|
488
|
+
* the base and the head) or `no_base` (the instance names no base to
|
|
489
|
+
* open against, so no create was tried). Absent on a plain check. */
|
|
490
|
+
unrecovered?: "no_commits" | "no_base";
|
|
491
|
+
}
|
|
446
492
|
| { state: "open"; prNumber: number; url: string; headSha?: string; autoMergeEnabled?: boolean }
|
|
447
493
|
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string };
|
|
448
494
|
|
|
@@ -458,6 +504,9 @@ export type StepReturn =
|
|
|
458
504
|
| { type: "read-record"; step: string; run: ChildFacts; at: number }
|
|
459
505
|
| { type: "pr-check"; step: string; pr: PrCheck; at: number }
|
|
460
506
|
| { type: "merge"; step: string; outcome: "merged"; sha: string; at: number }
|
|
507
|
+
// The door found the pull request already merged after the approval — auto-merge
|
|
508
|
+
// fired, or a person merged — so the runner merged nothing (`by: other`).
|
|
509
|
+
| { type: "merge"; step: string; outcome: "merged"; by: "other"; sha: string; mergedAt: string; at: number }
|
|
461
510
|
| { type: "merge"; step: string; outcome: "pending" | "refused"; reason: string; at: number }
|
|
462
511
|
| { type: "sleep"; step: string };
|
|
463
512
|
|
|
@@ -473,7 +522,12 @@ export type UnitEnding =
|
|
|
473
522
|
| { kind: "merge_ready"; pr: PrRef; reviewRounds: number }
|
|
474
523
|
| { kind: "merge_refused"; pr: PrRef; reason: string; reviewRounds: number }
|
|
475
524
|
| { kind: "round_cap"; maxRounds: number; reviewRounds: number }
|
|
476
|
-
| { kind: "wall_clock_cap"; remainingMs: number; reviewRounds: number }
|
|
525
|
+
| { kind: "wall_clock_cap"; remainingMs: number; reviewRounds: number; spent: ShipBudgetSpent }
|
|
526
|
+
/** The wall clock capped AFTER the coding child opened or updated the pull
|
|
527
|
+
* request: the work stands and only the review is missing, so the ending
|
|
528
|
+
* names the pull request and "review pending" instead of calling the unit a
|
|
529
|
+
* failure — the re-issued attempt starts at the review round (`lastPush`). */
|
|
530
|
+
| { kind: "review_pending"; pr: PrRef; headSha?: string; reviewRounds: number; spent: ShipBudgetSpent }
|
|
477
531
|
| {
|
|
478
532
|
kind: "stopped";
|
|
479
533
|
mode: "soft" | "hard";
|
|
@@ -494,6 +548,12 @@ export type CoordinatorNote =
|
|
|
494
548
|
| { type: "round"; index: number; agent: ChildPreset; outcome: ShipRoundOutcome }
|
|
495
549
|
| { type: "ended"; ending: UnitEnding };
|
|
496
550
|
|
|
551
|
+
/** How the pipeline's budget went, in ms: the coding rounds' (round 0 and the
|
|
552
|
+
* findings steps), the review rounds', and everything else (branching,
|
|
553
|
+
* pr-checks, busy waits, merge polls) — reported on the cap endings so a
|
|
554
|
+
* person can see whether the cap or the child is the problem. */
|
|
555
|
+
export type ShipBudgetSpent = Readonly<Record<"coding" | "review" | "waiting", number>>;
|
|
556
|
+
|
|
497
557
|
export interface UnitPipelineInput {
|
|
498
558
|
unit: { id: string; branch: string };
|
|
499
559
|
repo: string;
|
|
@@ -513,6 +573,11 @@ export interface UnitPipelineInput {
|
|
|
513
573
|
generated: boolean;
|
|
514
574
|
/** Resume at review: an open pull request of ship's own the requester named. */
|
|
515
575
|
resume?: { pr: number; headSha?: string; url?: string };
|
|
576
|
+
/** The head the previous attempt's coding child last pushed (a
|
|
577
|
+
* `review_pending` ending's `headSha`, carried on the unit's row): when the
|
|
578
|
+
* pre-check finds the open pull request still at exactly this head, there is
|
|
579
|
+
* nothing to code and the attempt starts at the review round. */
|
|
580
|
+
lastPush?: string;
|
|
516
581
|
}
|
|
517
582
|
|
|
518
583
|
type Phase =
|
|
@@ -524,7 +589,17 @@ type Phase =
|
|
|
524
589
|
/** `until`: when the child's budget plus the margin runs out, counted from the spawn's answer — the wait's last slice ends there. */
|
|
525
590
|
| { at: "wait"; round: RoundRef; runId: string; n: number; until: number }
|
|
526
591
|
| { at: "read"; round: RoundRef; runId: string; n: number; until: number }
|
|
527
|
-
| {
|
|
592
|
+
| {
|
|
593
|
+
at: "pr-check";
|
|
594
|
+
round: RoundRef;
|
|
595
|
+
runId: string;
|
|
596
|
+
childHead?: string;
|
|
597
|
+
finalReply?: string;
|
|
598
|
+
/** The coding child died (`failed` or `interrupted`) after it may have
|
|
599
|
+
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
600
|
+
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
601
|
+
dead?: "failed" | "interrupted";
|
|
602
|
+
}
|
|
528
603
|
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number }
|
|
529
604
|
| { at: "merge-sleep"; pr: PrRef; headSha: string; n: number; since: number }
|
|
530
605
|
| { at: "ended" };
|
|
@@ -551,6 +626,8 @@ export interface UnitPipelineState {
|
|
|
551
626
|
readonly findingsRunByRound: Readonly<Record<number, string>>;
|
|
552
627
|
/** The last coding run (round 0's child or a findings step's): its record carries the unit's handoff. */
|
|
553
628
|
readonly lastCodingRunId?: string;
|
|
629
|
+
/** How the budget went so far, accrued as each answer moves the clock. */
|
|
630
|
+
readonly spentMs: ShipBudgetSpent;
|
|
554
631
|
readonly ending?: UnitEnding;
|
|
555
632
|
}
|
|
556
633
|
|
|
@@ -579,6 +656,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
579
656
|
clock: at,
|
|
580
657
|
phase: { at: "pre-check" },
|
|
581
658
|
reviewRounds: 0,
|
|
659
|
+
spentMs: { coding: 0, review: 0, waiting: 0 },
|
|
582
660
|
findingsByRound: {},
|
|
583
661
|
dispositionsByRound: {},
|
|
584
662
|
reviewRunByRound: {},
|
|
@@ -609,8 +687,25 @@ function waitSliceMs(clock: number, until: number): number {
|
|
|
609
687
|
/** The child's budget: its preset's own, clipped to the pipeline's remaining
|
|
610
688
|
* wall clock (agent-ship item 8's clip), never under the two minutes a
|
|
611
689
|
* spawn accepts. */
|
|
612
|
-
function budgetMinutesFor(s: UnitPipelineState,
|
|
613
|
-
|
|
690
|
+
function budgetMinutesFor(s: UnitPipelineState, round: RoundRef): number {
|
|
691
|
+
const headroom = remainingMs(s) - reserveFor(round.kind);
|
|
692
|
+
return Math.max(2, Math.min(s.input.childMinutes[presetOf(round.kind)], Math.floor(headroom / MIN)));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** What a child of each round kind leaves on the pipeline's clock: round 0's
|
|
696
|
+
* coding child the whole loop (two reviews and the merge poll,
|
|
697
|
+
* SHIP_LOOP_RESERVE_MS), a findings child the re-review and the merge poll
|
|
698
|
+
* (SHIP_FIX_RESERVE_MS), a review child nothing — the review is what the
|
|
699
|
+
* reserve was held for. */
|
|
700
|
+
function reserveFor(kind: RoundKind): number {
|
|
701
|
+
switch (kind) {
|
|
702
|
+
case "coding":
|
|
703
|
+
return SHIP_LOOP_RESERVE_MS;
|
|
704
|
+
case "findings":
|
|
705
|
+
return SHIP_FIX_RESERVE_MS;
|
|
706
|
+
case "review":
|
|
707
|
+
return 0;
|
|
708
|
+
}
|
|
614
709
|
}
|
|
615
710
|
|
|
616
711
|
const roundStep = (s: UnitPipelineState, round: RoundRef) => `${s.input.unit.id}/${round.index}/${round.kind}`;
|
|
@@ -668,7 +763,7 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
668
763
|
step: roundStep(s, p.round),
|
|
669
764
|
preset,
|
|
670
765
|
round: p.round,
|
|
671
|
-
budgetMinutes: budgetMinutesFor(s,
|
|
766
|
+
budgetMinutes: budgetMinutesFor(s, p.round),
|
|
672
767
|
brief: briefFor(s, p.round),
|
|
673
768
|
};
|
|
674
769
|
}
|
|
@@ -687,7 +782,11 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
687
782
|
case "read":
|
|
688
783
|
return { type: "read-record", step: `${roundStep(s, p.round)}/read/${p.n}`, runId: p.runId };
|
|
689
784
|
case "pr-check":
|
|
690
|
-
return {
|
|
785
|
+
return {
|
|
786
|
+
type: "pr-check",
|
|
787
|
+
step: `${roundStep(s, p.round)}/pr-check`,
|
|
788
|
+
...(p.dead !== undefined ? { recover: { runId: p.runId } } : {}),
|
|
789
|
+
};
|
|
691
790
|
case "merge":
|
|
692
791
|
return { type: "merge", step: `${unit}/merge/${p.n}`, prNumber: p.pr.number, headSha: p.headSha };
|
|
693
792
|
case "merge-sleep":
|
|
@@ -717,12 +816,26 @@ const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): CoordinatorNote
|
|
|
717
816
|
outcome,
|
|
718
817
|
});
|
|
719
818
|
|
|
819
|
+
/** The cap's ending: `review_pending` when the clock ran out entering a
|
|
820
|
+
* review round with the child's pull request standing — the work shipped and
|
|
821
|
+
* only the review is missing — else the wall-clock cap. Both carry the split. */
|
|
822
|
+
function capEnding(s: UnitPipelineState, round?: RoundRef): UnitEnding {
|
|
823
|
+
if (round?.kind === "review" && s.pr !== undefined)
|
|
824
|
+
return {
|
|
825
|
+
kind: "review_pending",
|
|
826
|
+
pr: s.pr,
|
|
827
|
+
...(s.lastReviewHead !== undefined ? { headSha: s.lastReviewHead } : {}),
|
|
828
|
+
reviewRounds: s.reviewRounds,
|
|
829
|
+
spent: s.spentMs,
|
|
830
|
+
};
|
|
831
|
+
return { kind: "wall_clock_cap", remainingMs: remainingMs(s), reviewRounds: s.reviewRounds, spent: s.spentMs };
|
|
832
|
+
}
|
|
833
|
+
|
|
720
834
|
/** Start a round if the reservation holds (agent-ship item 8's check: a
|
|
721
835
|
* child clipped under the reserve cannot do useful work). */
|
|
722
836
|
function enterRound(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNote[] = []): Transition {
|
|
723
837
|
const remaining = remainingMs(s);
|
|
724
|
-
if (remaining < SHIP_ROUND_RESERVE_MS)
|
|
725
|
-
return end(s, { kind: "wall_clock_cap", remainingMs: remaining, reviewRounds: s.reviewRounds }, notes);
|
|
838
|
+
if (remaining < SHIP_ROUND_RESERVE_MS) return end(s, capEnding(s, round), notes);
|
|
726
839
|
const reviewRounds = round.kind === "review" ? round.index : s.reviewRounds;
|
|
727
840
|
return { state: { ...s, reviewRounds, phase: { at: "spawn", round, busy: 0 } }, notes };
|
|
728
841
|
}
|
|
@@ -773,17 +886,12 @@ function settleCoding(
|
|
|
773
886
|
},
|
|
774
887
|
[roundNote(round, "stopped")],
|
|
775
888
|
);
|
|
889
|
+
// A failed coding child no longer aborts outright: the pr-check looks at the
|
|
890
|
+
// branch first — a push before the death is recovered as the round's pull
|
|
891
|
+
// request (agent-ship items 10 and 15), and only a branch with
|
|
892
|
+
// nothing on it ends the unit with the child's own reason.
|
|
776
893
|
if (facts.status === "failed")
|
|
777
|
-
return
|
|
778
|
-
next,
|
|
779
|
-
{
|
|
780
|
-
kind: "aborted",
|
|
781
|
-
reason: `⚠️ The coding child of round ${round.index} (run ${runId}) ended \`failed\` — its run page has the error; nothing was opened or edited from it.`,
|
|
782
|
-
round,
|
|
783
|
-
reviewRounds: next.reviewRounds,
|
|
784
|
-
},
|
|
785
|
-
[roundNote(round, "aborted")],
|
|
786
|
-
);
|
|
894
|
+
return { state: { ...next, phase: { at: "pr-check", round, runId, dead: "failed" } }, notes: [] };
|
|
787
895
|
next = {
|
|
788
896
|
...next,
|
|
789
897
|
phase: {
|
|
@@ -919,6 +1027,32 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
919
1027
|
// completed without a pull request of its own, and the unit is done.
|
|
920
1028
|
if (pr.state === "merged") return foundMerged(s, pr, [roundNote(round, "completed")]);
|
|
921
1029
|
if (pr.state === "none") {
|
|
1030
|
+
// A dead child left nothing on the branch to recover: the unit ends with
|
|
1031
|
+
// the child's own reason — never the budget clip.
|
|
1032
|
+
if (phase.dead === "interrupted")
|
|
1033
|
+
return end(s, { kind: "interrupted", round, runId: phase.runId, reviewRounds: s.reviewRounds }, [
|
|
1034
|
+
roundNote(round, "aborted"),
|
|
1035
|
+
]);
|
|
1036
|
+
if (phase.dead === "failed") {
|
|
1037
|
+
// The abort repeats the bot's reason for recovering nothing, and claims
|
|
1038
|
+
// no more than the answer carried.
|
|
1039
|
+
const why =
|
|
1040
|
+
pr.unrecovered === "no_commits"
|
|
1041
|
+
? `nothing heads \`${s.input.unit.branch}\`: no commits were pushed, so there was no work to recover`
|
|
1042
|
+
: pr.unrecovered === "no_base"
|
|
1043
|
+
? `\`${s.input.unit.branch}\` could not be given a pull request: the instance names no base branch to open it against, so whatever was pushed stays on the branch`
|
|
1044
|
+
: `the pr-check found no pull request heading \`${s.input.unit.branch}\`, so nothing was recovered`;
|
|
1045
|
+
return end(
|
|
1046
|
+
s,
|
|
1047
|
+
{
|
|
1048
|
+
kind: "aborted",
|
|
1049
|
+
reason: `⚠️ The coding child of round ${round.index} (run ${phase.runId}) ended \`failed\` — its run page has the error — and ${why}.`,
|
|
1050
|
+
round,
|
|
1051
|
+
reviewRounds: s.reviewRounds,
|
|
1052
|
+
},
|
|
1053
|
+
[roundNote(round, "aborted")],
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
922
1056
|
const reason =
|
|
923
1057
|
round.index === 0
|
|
924
1058
|
? `⚠️ Ship ended at round 0: the coding round ended without opening a pull request (a clarifying question, a budget write-up, an unproven push or a description-less push ends the pipeline here). No review round ran.`
|
|
@@ -944,6 +1078,25 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
944
1078
|
const codingHead = normalizeHead(head);
|
|
945
1079
|
const reviewedAt = normalizeHead(s.lastReviewHead);
|
|
946
1080
|
if (codingHead !== undefined && reviewedAt !== undefined && sameCommit(codingHead, reviewedAt)) {
|
|
1081
|
+
// A findings child that died before it pushed: the recover pr-check found
|
|
1082
|
+
// the round's own pull request, still at the reviewed head. The unit
|
|
1083
|
+
// ends with the child's own reason — the ship-restart note for a bot
|
|
1084
|
+
// roll, the failure for a failed run — never as the round's inaction.
|
|
1085
|
+
if (phase.dead === "interrupted")
|
|
1086
|
+
return end(next, { kind: "interrupted", round, runId: phase.runId, reviewRounds: next.reviewRounds }, [
|
|
1087
|
+
roundNote(round, "aborted"),
|
|
1088
|
+
]);
|
|
1089
|
+
if (phase.dead === "failed")
|
|
1090
|
+
return end(
|
|
1091
|
+
next,
|
|
1092
|
+
{
|
|
1093
|
+
kind: "aborted",
|
|
1094
|
+
reason: `⚠️ The findings child of round ${round.index} (run ${phase.runId}) ended \`failed\` — its run page has the error — and the branch still sits at \`${codingHead.slice(0, 7)}\`, the commit the review already read, so nothing new was pushed to re-review.`,
|
|
1095
|
+
round,
|
|
1096
|
+
reviewRounds: next.reviewRounds,
|
|
1097
|
+
},
|
|
1098
|
+
[roundNote(round, "aborted")],
|
|
1099
|
+
);
|
|
947
1100
|
const findings = s.findingsByRound[round.index] ?? [];
|
|
948
1101
|
const dispositions = s.dispositionsByRound[round.index] ?? [];
|
|
949
1102
|
const allDeclined =
|
|
@@ -967,6 +1120,17 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
967
1120
|
]);
|
|
968
1121
|
}
|
|
969
1122
|
|
|
1123
|
+
/** Move the clock and charge the elapsed time to the budget's bucket: a phase
|
|
1124
|
+
* inside a coding or findings round is the coding child's time, a review
|
|
1125
|
+
* round's is the review's, everything else — the pre-check, the branch, busy
|
|
1126
|
+
* waits, the merge polls — is waiting. The split rides the cap endings. */
|
|
1127
|
+
function withClock(s: UnitPipelineState, at: number): UnitPipelineState {
|
|
1128
|
+
const delta = Math.max(0, at - s.clock);
|
|
1129
|
+
const round = "round" in s.phase ? s.phase.round : undefined;
|
|
1130
|
+
const bucket = round === undefined ? "waiting" : round.kind === "review" ? "review" : "coding";
|
|
1131
|
+
return { ...s, clock: at, spentMs: { ...s.spentMs, [bucket]: s.spentMs[bucket] + delta } };
|
|
1132
|
+
}
|
|
1133
|
+
|
|
970
1134
|
/**
|
|
971
1135
|
* Feed a step's answer to the machine. An answer for any step but the one the
|
|
972
1136
|
* machine is at — a duplicate `run finished`, a replayed spawn — changes
|
|
@@ -976,7 +1140,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
976
1140
|
const expected = nextAction(s);
|
|
977
1141
|
if (expected.type === "end" || ret.step !== expected.step || ret.type !== expected.type)
|
|
978
1142
|
return { state: s, notes: [] };
|
|
979
|
-
const clocked: UnitPipelineState = "at" in ret ?
|
|
1143
|
+
const clocked: UnitPipelineState = "at" in ret ? withClock(s, ret.at) : s;
|
|
980
1144
|
const p = s.phase;
|
|
981
1145
|
switch (p.at) {
|
|
982
1146
|
case "pre-check": {
|
|
@@ -987,6 +1151,20 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
987
1151
|
// by the coding child and adopted at the round's own pr-check.
|
|
988
1152
|
const r = ret as Extract<StepReturn, { type: "pr-check" }>;
|
|
989
1153
|
if (r.pr.state === "merged") return foundMerged(clocked, r.pr);
|
|
1154
|
+
// The open pull request still heads at the child's own last push (the
|
|
1155
|
+
// previous attempt ended `review_pending`): nothing to code, so the
|
|
1156
|
+
// attempt adopts it and starts at the review round — never a fresh
|
|
1157
|
+
// coding round on an already-shipped pull request.
|
|
1158
|
+
if (r.pr.state === "open") {
|
|
1159
|
+
const head = normalizeHead(r.pr.headSha);
|
|
1160
|
+
const lastPush = normalizeHead(s.input.lastPush);
|
|
1161
|
+
if (head !== undefined && lastPush !== undefined && sameCommit(head, lastPush))
|
|
1162
|
+
return nextReview({
|
|
1163
|
+
...clocked,
|
|
1164
|
+
pr: { number: r.pr.prNumber, url: r.pr.url },
|
|
1165
|
+
lastReviewHead: head,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
990
1168
|
return { state: { ...clocked, phase: { at: "branch" } }, notes: [] };
|
|
991
1169
|
}
|
|
992
1170
|
case "branch": {
|
|
@@ -1004,7 +1182,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1004
1182
|
case "spawned":
|
|
1005
1183
|
case "alreadySpawned": {
|
|
1006
1184
|
// The child's budget runs from the spawn's answer; the wait walks it, plus the margin, in chunks.
|
|
1007
|
-
const until = clocked.clock + budgetMinutesFor(s,
|
|
1185
|
+
const until = clocked.clock + budgetMinutesFor(s, p.round) * MIN + WAIT_MARGIN_MS;
|
|
1008
1186
|
const runs =
|
|
1009
1187
|
p.round.kind === "review"
|
|
1010
1188
|
? { reviewRunByRound: { ...s.reviewRunByRound, [p.round.index]: r.runId } }
|
|
@@ -1022,12 +1200,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1022
1200
|
case "busy": {
|
|
1023
1201
|
// Another run holds the unit's thread: wait for its end, then ask
|
|
1024
1202
|
// again — unless the pipeline's wall clock ran out meanwhile.
|
|
1025
|
-
if (remainingMs(clocked) < SHIP_ROUND_RESERVE_MS)
|
|
1026
|
-
return end(clocked, {
|
|
1027
|
-
kind: "wall_clock_cap",
|
|
1028
|
-
remainingMs: remainingMs(clocked),
|
|
1029
|
-
reviewRounds: s.reviewRounds,
|
|
1030
|
-
});
|
|
1203
|
+
if (remainingMs(clocked) < SHIP_ROUND_RESERVE_MS) return end(clocked, capEnding(clocked, p.round));
|
|
1031
1204
|
return {
|
|
1032
1205
|
state: {
|
|
1033
1206
|
...clocked,
|
|
@@ -1076,10 +1249,19 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1076
1249
|
},
|
|
1077
1250
|
notes: [],
|
|
1078
1251
|
};
|
|
1079
|
-
if (r.run.status === "interrupted")
|
|
1252
|
+
if (r.run.status === "interrupted") {
|
|
1253
|
+
// A dead CODING child may have pushed before the ledger closed it: the
|
|
1254
|
+
// pr-check recovers the branch. A review child has nothing on the
|
|
1255
|
+
// branch to recover, so its interruption still ends the unit at once.
|
|
1256
|
+
if (p.round.kind !== "review")
|
|
1257
|
+
return {
|
|
1258
|
+
state: { ...clocked, phase: { at: "pr-check", round: p.round, runId: p.runId, dead: "interrupted" } },
|
|
1259
|
+
notes: [],
|
|
1260
|
+
};
|
|
1080
1261
|
return end(clocked, { kind: "interrupted", round: p.round, runId: p.runId, reviewRounds: s.reviewRounds }, [
|
|
1081
1262
|
roundNote(p.round, "aborted"),
|
|
1082
1263
|
]);
|
|
1264
|
+
}
|
|
1083
1265
|
return p.round.kind === "review"
|
|
1084
1266
|
? settleReview(clocked, p.round, r.run)
|
|
1085
1267
|
: settleCoding(clocked, p.round, p.runId, r.run);
|
|
@@ -1089,7 +1271,18 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1089
1271
|
case "merge": {
|
|
1090
1272
|
const r = ret as Extract<StepReturn, { type: "merge" }>;
|
|
1091
1273
|
if (r.outcome === "merged")
|
|
1092
|
-
|
|
1274
|
+
// Found already merged at the door — auto-merge or a person, after the
|
|
1275
|
+
// approval: the unit is done, the runner merged nothing (`by: other`).
|
|
1276
|
+
return "by" in r
|
|
1277
|
+
? end(clocked, {
|
|
1278
|
+
kind: "merged",
|
|
1279
|
+
by: "other",
|
|
1280
|
+
pr: p.pr,
|
|
1281
|
+
sha: r.sha,
|
|
1282
|
+
mergedAt: r.mergedAt,
|
|
1283
|
+
reviewRounds: s.reviewRounds,
|
|
1284
|
+
})
|
|
1285
|
+
: end(clocked, { kind: "merged", by: "runner", pr: p.pr, sha: r.sha, reviewRounds: s.reviewRounds });
|
|
1093
1286
|
if (r.outcome === "refused")
|
|
1094
1287
|
return end(clocked, { kind: "merge_refused", pr: p.pr, reason: r.reason, reviewRounds: s.reviewRounds });
|
|
1095
1288
|
const waited = r.at - p.since;
|
|
@@ -1135,6 +1328,12 @@ function dispositionFor(s: UnitPipelineState, finding: Finding, round: number):
|
|
|
1135
1328
|
return undefined;
|
|
1136
1329
|
}
|
|
1137
1330
|
|
|
1331
|
+
/** How the budget went, in the card's words: coding, review, waiting minutes. */
|
|
1332
|
+
function budgetSplitLine(spent: ShipBudgetSpent, maxMinutes: number): string {
|
|
1333
|
+
const min = (ms: number) => Math.round(ms / MIN);
|
|
1334
|
+
return `Budget split (${maxMinutes} min): coding ${min(spent.coding)} min, review ${min(spent.review)} min, waiting ${min(spent.waiting)} min.`;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1138
1337
|
/** The cap report's declined-vs-unaddressed split over the last review round's findings. */
|
|
1139
1338
|
function splitReport(s: UnitPipelineState): string {
|
|
1140
1339
|
if (s.reviewRounds === 0) return "No review round ran before the cap — there are no findings to report.";
|
|
@@ -1222,9 +1421,16 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1222
1421
|
case "wall_clock_cap":
|
|
1223
1422
|
return join([
|
|
1224
1423
|
`🧢 Ship stopped at a cap: the remaining pipeline time (~${Math.max(0, Math.round(e.remainingMs / MIN))} min of the ${s.input.caps.maxMinutes}-minute budget) cannot hold another round — no approval after ${rounds}.${prLine}`,
|
|
1424
|
+
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
1225
1425
|
splitReport(s),
|
|
1226
1426
|
reissue,
|
|
1227
1427
|
]);
|
|
1428
|
+
case "review_pending":
|
|
1429
|
+
return join([
|
|
1430
|
+
`⏳ Review pending: the coding child shipped ${e.pr.url}${e.headSha !== undefined ? ` (head \`${e.headSha.slice(0, 7)}\`)` : ""} but the remaining pipeline time cannot hold the review round — the work stands, only the review is missing. The next attempt starts at the review round while the pull request still heads at the child's own last push.`,
|
|
1431
|
+
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
1432
|
+
reissue,
|
|
1433
|
+
]);
|
|
1228
1434
|
case "stopped":
|
|
1229
1435
|
return join([
|
|
1230
1436
|
`${e.mode === "hard" ? "⛔" : "⏹"} Ship stopped by operator (${e.mode} stop) after ${rounds}.${prLine}`,
|
|
@@ -68,6 +68,11 @@ export interface SpanOptions {
|
|
|
68
68
|
* plain fetch it was. */
|
|
69
69
|
export interface TraceOptions {
|
|
70
70
|
span?: Span;
|
|
71
|
+
/** Which door the call came through when it was not the surface's own
|
|
72
|
+
* grammar: `route` for a command the request router bound from prose
|
|
73
|
+
* (record 0036). Copied onto the registry's audit line, never read by a
|
|
74
|
+
* handler. Absent for a typed command. */
|
|
75
|
+
source?: "route";
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
export interface Span {
|
|
@@ -299,6 +299,14 @@ export function parseDepsStoreListing(stdout: string): DepsStoreListing {
|
|
|
299
299
|
* attaches. Raise with the instance, never by feel. */
|
|
300
300
|
export const DEPS_STORE_MAX_UNREFERENCED = 1;
|
|
301
301
|
|
|
302
|
+
/** How many spares the attach's disk-pressure path keeps: none. A spare is a
|
|
303
|
+
* warm cache for a lockfile key no live tree references, worth minutes on
|
|
304
|
+
* the next attach at that key; a tree refused for want of space is a run
|
|
305
|
+
* falling to a cold sandbox now. Under pressure the spare goes first, before
|
|
306
|
+
* any idle tree (docs/reference/specs/resident-repos.md item 55), and its
|
|
307
|
+
* entry backup with it — the next attach at that key restores or installs. */
|
|
308
|
+
export const DEPS_STORE_MAX_UNREFERENCED_UNDER_PRESSURE = 0;
|
|
309
|
+
|
|
302
310
|
/** Eviction candidates coldest first. Never a protected key (the checkout's,
|
|
303
311
|
* a live binding's, an install in flight). Debris — an incomplete entry
|
|
304
312
|
* with nothing in flight for it — is always first: it is half an install
|
|
@@ -318,6 +326,9 @@ export interface DepsEvictionPlan {
|
|
|
318
326
|
remove: string[];
|
|
319
327
|
/** Keys that stay, for the log. */
|
|
320
328
|
keep: string[];
|
|
329
|
+
/** The entries `remove` names (debris first, then the coldest spares), with
|
|
330
|
+
* their measured sizes — what a pressure eviction gives back. */
|
|
331
|
+
evicted: DepsStoreEntry[];
|
|
321
332
|
}
|
|
322
333
|
|
|
323
334
|
export function planDepsEviction(input: {
|
|
@@ -336,7 +347,8 @@ export function planDepsEviction(input: {
|
|
|
336
347
|
const evictSpares = spares.slice(0, Math.max(0, spares.length - max));
|
|
337
348
|
const remove = [...debris, ...evictSpares].map((e) => depsEntryPath(e.key, storeDir));
|
|
338
349
|
remove.push(...input.leftovers);
|
|
339
|
-
const
|
|
350
|
+
const evicted = [...debris, ...evictSpares];
|
|
351
|
+
const removed = new Set(evicted.map((e) => e.key));
|
|
340
352
|
const keep = input.entries.filter((e) => !removed.has(e.key)).map((e) => e.key);
|
|
341
|
-
return { remove, keep };
|
|
353
|
+
return { remove, keep, evicted };
|
|
342
354
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// the disk. Two facts fix that: the failure is classified `disk-full` (never
|
|
12
12
|
// serviceable, so the bot skips the attach and the card names the disk), and
|
|
13
13
|
// the resident recycles its container — the disk is a cache; the next refresh
|
|
14
|
-
// cycle restores mirror + checkout from R2 — once
|
|
14
|
+
// cycle restores mirror + checkout from R2 — once no run is using it.
|
|
15
15
|
|
|
16
16
|
/** The errno wording tools print for ENOSPC: Node's `ENOSPC` code and libc's
|
|
17
17
|
* strerror text (git, cp, tar, pnpm all pass it through). A message carrying
|
|
@@ -76,17 +76,27 @@ export const DISK_FULL_RECYCLE_COOLDOWN_MS = 60 * 60_000;
|
|
|
76
76
|
export type DiskFullRecovery = { action: "recycle" } | { action: "wait"; why: string };
|
|
77
77
|
|
|
78
78
|
/** Whether a disk-full resident may stop its container now. The disk is a
|
|
79
|
-
* cache
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
79
|
+
* cache — the next cycle restores mirror + checkout from R2, and a live
|
|
80
|
+
* binding's tree is recreated on its next attach — so the one question is
|
|
81
|
+
* whether a RUN is using it, never what the trees hold (item 17: a run
|
|
82
|
+
* starts from a clean tree; what it wants kept, it commits and pushes). Two
|
|
83
|
+
* facts say a run may be: an operation in flight (a recycle kills it), and a
|
|
84
|
+
* live binding attached to or used within the idle floor — the op counter is
|
|
85
|
+
* 0 between a run's tool calls, so the recent-use floor is what stands for a
|
|
86
|
+
* run mid-flight. That is the idle-sleep gate's own predicate (item 16b): a
|
|
87
|
+
* platform sleep destroys the disk exactly as a recycle does, so one rule
|
|
88
|
+
* says when the disk may go away. Both keep the container; so does the
|
|
89
|
+
* cooldown. */
|
|
85
90
|
export function planDiskFullRecovery(input: {
|
|
86
91
|
now: number;
|
|
87
92
|
lastRecycleAt?: number;
|
|
88
93
|
inFlight: number;
|
|
89
|
-
|
|
94
|
+
/** A live binding was attached to or used (an exec bumps `lastAttachAt`
|
|
95
|
+
* too) within `idleFloorS` — computed once by the Worker, for the idle
|
|
96
|
+
* gate and this plan alike. */
|
|
97
|
+
recentlyUsed: boolean;
|
|
98
|
+
/** The floor `recentlyUsed` was measured against, named in the refusal. */
|
|
99
|
+
idleFloorS: number;
|
|
90
100
|
}): DiskFullRecovery {
|
|
91
101
|
if (input.lastRecycleAt !== undefined && input.now - input.lastRecycleAt < DISK_FULL_RECYCLE_COOLDOWN_MS) {
|
|
92
102
|
const min = Math.round((input.now - input.lastRecycleAt) / 60_000);
|
|
@@ -97,10 +107,11 @@ export function planDiskFullRecovery(input: {
|
|
|
97
107
|
}
|
|
98
108
|
if (input.inFlight > 0)
|
|
99
109
|
return { action: "wait", why: `${input.inFlight} operation(s) in flight — a recycle would kill them` };
|
|
100
|
-
if (
|
|
110
|
+
if (input.recentlyUsed) {
|
|
111
|
+
const min = Math.round(input.idleFloorS / 60);
|
|
101
112
|
return {
|
|
102
113
|
action: "wait",
|
|
103
|
-
why:
|
|
114
|
+
why: `a live worktree was attached to or used within the last ${min} min — a run may be mid-flight between two tool calls, and a recycle would destroy its tree`,
|
|
104
115
|
};
|
|
105
116
|
}
|
|
106
117
|
return { action: "recycle" };
|