@coreplane/switchboard 1.234.0 → 1.236.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/gc.ts +6 -9
- package/dist/assets/deploy/cloudflare-resident/worker.ts +225 -119
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +143 -13
- 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/coordinator/contract.ts +24 -4
- package/dist/assets/src/core/coordinator/driver.ts +31 -3
- package/dist/assets/src/core/runEvents.ts +43 -1
- 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 +213 -29
- package/dist/assets/src/core/trace/types.ts +5 -0
- package/dist/assets/src/execution/residentCleanliness.ts +24 -0
- package/dist/assets/src/execution/residentDisk.ts +21 -10
- package/dist/assets/src/execution/residentDiskBudget.ts +7 -6
- package/dist/assets/src/execution/sandboxErrors.ts +77 -0
- package/dist/assets/src/execution/sandboxIdle.ts +220 -0
- package/dist/assets/src/execution/sandboxStart.ts +90 -0
- package/dist/assets/web/dist/.vite/manifest.json +437 -422
- package/dist/assets/web/dist/assets/{AppShell-lYVcj-k7.js → AppShell-DZdz_sl7.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-D-v8am88.js → CostsPage-BfJsol9y.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-CvlWP7Eq.js → DeliveryPage-CvInfu7y.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-BzWS4Ca1.js → NotFoundPage-d37EP8hS.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DQSD0SZD.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CZUGLen6.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-Bngn4GjF.js +9 -0
- package/dist/assets/web/dist/assets/RunRoutePage-a-tMrirB.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-s0VKmRda.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-DEICQ4FY.js → RunsTabs-CyCdOhJ2.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-ByCmyrDs.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DhKEokGK.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-Ct6AtHhS.js → StatusDot-D-BPSPpw.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-qol5Boof.js → Tooltip-DPDx5tXT.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-0eiekTHQ.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Cw130Zyi.js → angular-html-Ni1Xil2G.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DY1m4C2s.js → angular-ts-oCgo6p05.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-W6Ty7v05.js → apl-CiWYl0gh.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DlLVHwVk.js → astro-B-VeKZ_J.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-Bco3tDh4.js → blade-BIWEISKd.js} +1 -1
- package/dist/assets/web/dist/assets/{c-1LaFY_cj.js → c-DHxA5Byp.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-B-OvrOL5.js → chapel-D4qGLw-Q.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-Ch3EPlHu.js → cobol-BznX5Q-u.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-hZuITnto.js → coffee-CVr8Idlj.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-B8iRVo8m.js → cpp-wrk6NfW4.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DKveI7lr.js → crystal-CGYWMeH1.js} +1 -1
- package/dist/assets/web/dist/assets/{css-_2_CD-rr.js → css-BgGytMsy.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-DwtJj-ou.js → dist-DSkVB16z.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-DYogEPEk.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BenMDPct.js → edge-CdR6AU9v.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-CBbdEqG6.js → elixir-CQRb3PZx.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DMub_2lY.js → elm-CpuA6W8B.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CI9He_Up.js → erb-CvlrblqH.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-COnovCVc.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-CR9Fxrau.js → glimmer-js-C-DS4g_J.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CwFwX_XS.js → glimmer-ts-DTkorWRI.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Daiodp1r.js → glsl-CH51-JW7.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-DZvrsEkB.js → graphql-BO-XymNv.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BaXOzdg7.js → hack-C-N0oicf.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CMskq_VA.js → haml-BfmdtWoP.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-Dhw_f7jD.js → handlebars-BqhO_te-.js} +1 -1
- package/dist/assets/web/dist/assets/{html-C9k99z0z.js → html-DUEZlTKV.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-VckPItXN.js → html-derivative-DOUJTS8A.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXU_3h9v.js → http-j7UH0Cqv.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-KU24JNdp.js → hurl-D6ZaBLJk.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BjXBzI5Z.js +1 -0
- package/dist/assets/web/dist/assets/{java-BbZjTNgf.js → java-dugWx3wi.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-DXCdqcZZ.js → javascript-CK4GC4JO.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-pDlsjTVp.js → jinja-DrLKIyKG.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DsXDk66e.js → jison-CS1Th-TO.js} +1 -1
- package/dist/assets/web/dist/assets/{json-tCxXfRgG.js → json-CkBqdVys.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-B0ZYkCmi.js → jsx-CCxdg7n3.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-BLzzeI6x.js → julia-BalKW2hA.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DvD2_60c.js → just-BKw1G40D.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-511zn3h7.js → latex-Dfad6ZN-.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BD0BkMox.js → liquid-f_CMGBvd.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-BcMK_uKM.js} +1 -1
- package/dist/assets/web/dist/assets/main-B77vAZVg.css +1 -0
- package/dist/assets/web/dist/assets/main-DR8iKpNt.js +28 -0
- package/dist/assets/web/dist/assets/{marko-1-G_nEXK.js → marko-B0oNLx2i.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DUe3AQLp.js → mdc-Bc_MmIyq.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Dg379_bA.js → nginx-Cmxv8RIt.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BnFq97ZO.js → nim-BtgRfaY0.js} +1 -1
- package/dist/assets/web/dist/assets/{org-F0uiwGvq.js → org-Dl9EzlHh.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-B0euczkl.js → perl-BmF7x7BB.js} +1 -1
- package/dist/assets/web/dist/assets/{php-DkL3k_n7.js → php-rH6CVptQ.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-L_OjjZP4.js → pug-CIN5Ccck.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BMjB00Zz.js → qml-BpLS3RFr.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DoeLdnqR.js → r-BUMF3B-H.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BzYNkAWy.js → razor-B7sTBPdT.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CPmElMk3.js → regexp-h2SvwrsJ.js} +1 -1
- package/dist/assets/web/dist/assets/residentsModel-VSJEepL1.js +1 -0
- package/dist/assets/web/dist/assets/{rst-DrsjgfI7.js → rst-DLOWEsD2.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cB42ppvx.js → ruby-Cl_-I4k3.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-R5Y3NM_I.js → sas-Cm4M-BvV.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-C-zWxV9s.js → scss-CEFCEQUF.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-oJF96aAU.js → shellscript-CE6zb5eS.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CznECKyi.js → shellsession-B4fDVzrM.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-wjHLSRag.js → soy-DwXCMpjE.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-C_BM8IOW.js → sql-DrnAKnyD.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-D31HO8aQ.js → stata-BzlwPWD6.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CKCLyplA.js → surrealql-CeUieZXd.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-3geWk2iu.js → svelte-BERIpIKR.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DiJr_hTD.js → templ-6FSuUDwg.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BEJGWyeF.js → tex-SWCyMwNX.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-C8Mzdpxx.js → ts-tags-VWj7gWgY.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-UQiSm_p3.js → tsx-B6dzfL8L.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-_Pvzkeo9.js → twig-BDXsr482.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BWMkINKK.js → typescript-s100Dt8y.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Bdg9m-e7.js → typst-WfyEAUdt.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CDZrrAZ8.js → vue-CkXOOJ-6.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-tAlpNhfN.js → vue-html-CEpzdcau.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-C27UF_rh.js → vue-vine-D9dsfiFA.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-CxeDr9Zh.js → xml-iBDofbJq.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bpi0uUnM.js → xsl-CqbJF4hD.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BtUgZ1GN.js → yaml-D9i6OxYe.js} +1 -1
- package/dist/cli.js +2336 -1192
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-C_WNBUao.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-D1sTyaZ_.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-C_bcTSjA.js +0 -5
- package/dist/assets/web/dist/assets/RunRoutePage-BWTi_6PG.js +0 -10
- package/dist/assets/web/dist/assets/RunsIndexPage-H6GkSv0a.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-BLcPzm1S.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CIhTEwN7.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-CRtlGxRm.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
|
@@ -356,6 +356,21 @@ export interface SpanEndEvent {
|
|
|
356
356
|
at?: number;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
/** A value a `route` event may carry in a bound command input: JSON, nested
|
|
360
|
+
* at most three objects deep under `options`, and nothing else. The record
|
|
361
|
+
* crosses the run store's RPC boundary, whose typing walks every field: a
|
|
362
|
+
* field of `unknown` types the whole record `never`, and a recursive alias is
|
|
363
|
+
* "excessively deep" to it, so the nesting is spelled out level by level. The
|
|
364
|
+
* model's tool call is JSON to begin with; the registry's option schemas nest
|
|
365
|
+
* one object deep (`models.coding`); a deeper value is stored as its JSON
|
|
366
|
+
* text, still redacted and capped. */
|
|
367
|
+
export type RouteInputLeaf = string | number | boolean | null;
|
|
368
|
+
export type RouteInputLeafOrList = RouteInputLeaf | ReadonlyArray<RouteInputLeaf>;
|
|
369
|
+
export type RouteInputObject1 = { readonly [key: string]: RouteInputLeafOrList };
|
|
370
|
+
export type RouteInputObject2 = { readonly [key: string]: RouteInputLeafOrList | RouteInputObject1 };
|
|
371
|
+
export type RouteInputObject3 = { readonly [key: string]: RouteInputLeafOrList | RouteInputObject2 };
|
|
372
|
+
export type RouteInputValue = RouteInputLeafOrList | RouteInputObject3;
|
|
373
|
+
|
|
359
374
|
export type RunEvent =
|
|
360
375
|
/** `callId` is the provider's tool_use id — the explicit pair key between a
|
|
361
376
|
* call and its result (live-view item 13); the runner stamps it on both. */
|
|
@@ -367,6 +382,11 @@ export type RunEvent =
|
|
|
367
382
|
* past the summary cap. The status card and friction analyzer keep reading
|
|
368
383
|
* `summary`. */
|
|
369
384
|
/** `spanId` (docs/reference/specs/tracing.md): the `tool.*` span this call ran under, once the runner emits spans. */
|
|
385
|
+
/** `logIndex` (docs/reference/specs/run-history.md item 53): the row of the run's
|
|
386
|
+
* session log holding the assistant turn this call rode in — on a `tool_result`,
|
|
387
|
+
* the user turn its batch's results make. How a search hit's turn finds its
|
|
388
|
+
* step on the page. Absent for a run without a session, a record from before
|
|
389
|
+
* the field, and a call the mirror could not place. */
|
|
370
390
|
| {
|
|
371
391
|
type: "tool_call";
|
|
372
392
|
tool: string;
|
|
@@ -374,6 +394,7 @@ export type RunEvent =
|
|
|
374
394
|
command?: string;
|
|
375
395
|
callId?: string;
|
|
376
396
|
spanId?: string;
|
|
397
|
+
logIndex?: number;
|
|
377
398
|
seq?: number;
|
|
378
399
|
at?: number;
|
|
379
400
|
}
|
|
@@ -393,6 +414,7 @@ export type RunEvent =
|
|
|
393
414
|
output?: string;
|
|
394
415
|
infra?: true;
|
|
395
416
|
spanId?: string;
|
|
417
|
+
logIndex?: number;
|
|
396
418
|
seq?: number;
|
|
397
419
|
at?: number;
|
|
398
420
|
}
|
|
@@ -628,6 +650,16 @@ export type RunEvent =
|
|
|
628
650
|
seq?: number;
|
|
629
651
|
at?: number;
|
|
630
652
|
}
|
|
653
|
+
/** The coordinator tag as a fact of the run (docs/reference/specs/run-history.md
|
|
654
|
+
* item 48a): the instance the run is a child of, the unit its idempotency
|
|
655
|
+
* key named, and the base branch its pull request targets — published by
|
|
656
|
+
* the dispatcher once at dispatch, from `DispatchOptions.coordinator`, so
|
|
657
|
+
* a run re-attached or restarted after a bot roll (whose spawn options are
|
|
658
|
+
* gone with the process) reads the plan's base back off its own events
|
|
659
|
+
* instead of letting the binding ref — the unit branch itself — stand in.
|
|
660
|
+
* `base` is absent when the spawn knew none; the post-step then falls to
|
|
661
|
+
* the coordinator store's `instance.base`. Additive: unknown → ignored. */
|
|
662
|
+
| { type: "coordinator_tag"; parentInstanceId: string; unit?: string; base?: string; seq?: number; at?: number }
|
|
631
663
|
/** The review post-step's outcome when the verdict landed
|
|
632
664
|
* (docs/reference/specs/agent-review.md item 18): the pull request it was
|
|
633
665
|
* posted to, the head it was pinned to (the carried head after a rebase,
|
|
@@ -672,7 +704,14 @@ export type RunEvent =
|
|
|
672
704
|
* `run_meta.agentSource` stays `default`). Published by the dispatcher
|
|
673
705
|
* straight to the registry right after `run_meta`, once per run the router
|
|
674
706
|
* answered; absent on every run a directive, a sticky preset or a scope
|
|
675
|
-
* chose. Head material, like `run_meta`. Additive: unknown → ignored.
|
|
707
|
+
* chose. Head material, like `run_meta`. Additive: unknown → ignored.
|
|
708
|
+
* A command the router bound from prose (record 0036: the door offers every
|
|
709
|
+
* chat command as a tool beside `route`) rides the command run's record
|
|
710
|
+
* instead, right after its `run_meta`: `preset` is the command run's agent
|
|
711
|
+
* (`command`), `command` the id the model called, `input` the bound input
|
|
712
|
+
* (redacted, each value capped) and `receipt` the chat form the reply led
|
|
713
|
+
* with (`routed: <chat form>`, redacted and capped at `ROUTE_RECEIPT_CAP`);
|
|
714
|
+
* how the invoke ended is the run's own status and `answer`. */
|
|
676
715
|
| {
|
|
677
716
|
type: "route";
|
|
678
717
|
preset: string;
|
|
@@ -680,6 +719,9 @@ export type RunEvent =
|
|
|
680
719
|
model: string;
|
|
681
720
|
parts?: ReadonlyArray<{ preset: string; text: string }>;
|
|
682
721
|
collapsed?: { presets: ReadonlyArray<string> };
|
|
722
|
+
command?: string;
|
|
723
|
+
input?: { readonly [key: string]: RouteInputValue };
|
|
724
|
+
receipt?: string;
|
|
683
725
|
seq?: number;
|
|
684
726
|
at?: number;
|
|
685
727
|
}
|
|
@@ -421,6 +421,7 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
421
421
|
ev.type === "review_artifact" ||
|
|
422
422
|
ev.type === "pr_description" ||
|
|
423
423
|
ev.type === "pr_opened" ||
|
|
424
|
+
ev.type === "coordinator_tag" ||
|
|
424
425
|
ev.type === "review_posted" ||
|
|
425
426
|
ev.type === "ship_round" ||
|
|
426
427
|
ev.type === "route" ||
|
|
@@ -427,7 +427,9 @@ function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
|
|
|
427
427
|
const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
|
|
428
428
|
return (
|
|
429
429
|
`Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
|
|
430
|
-
`the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop
|
|
430
|
+
`the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
|
|
431
|
+
`Right before the push, fetch ${onto} again and rebase once more if it moved during verify, ` +
|
|
432
|
+
`so the pull request is not born conflicting.`
|
|
431
433
|
);
|
|
432
434
|
}
|
|
433
435
|
|
|
@@ -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
|
|
@@ -407,7 +430,15 @@ export type CoordinatorAction =
|
|
|
407
430
|
| { type: "spawn"; step: string; preset: ChildPreset; round: RoundRef; budgetMinutes: number; brief: Brief }
|
|
408
431
|
| { type: "wait"; step: string; runId: string; timeoutMs: number }
|
|
409
432
|
| { type: "read-record"; step: string; runId: string }
|
|
410
|
-
| {
|
|
433
|
+
| {
|
|
434
|
+
type: "pr-check";
|
|
435
|
+
step: string;
|
|
436
|
+
/** Set after a coding child died: the bot opens the pull request from the
|
|
437
|
+
* pushed branch itself (title from the unit, body from this run's
|
|
438
|
+
* submitted description when the record holds one) instead of answering
|
|
439
|
+
* `none` over stranded work. */
|
|
440
|
+
recover?: { runId: string };
|
|
441
|
+
}
|
|
411
442
|
| { type: "merge"; step: string; prNumber: number; headSha: string }
|
|
412
443
|
| { type: "sleep"; step: string; ms: number }
|
|
413
444
|
| { type: "end"; step: string; ending: UnitEnding };
|
|
@@ -442,7 +473,14 @@ export type ChildFacts =
|
|
|
442
473
|
/** What heads the unit's branch on GitHub: nothing, an open pull request, or —
|
|
443
474
|
* with no open one — a merged one, `sha` the merge commit on the base. */
|
|
444
475
|
export type PrCheck =
|
|
445
|
-
| {
|
|
476
|
+
| {
|
|
477
|
+
state: "none";
|
|
478
|
+
/** After a recover pr-check (a dead coding child): why nothing was
|
|
479
|
+
* recovered — `no_commits` (GitHub refused the create: nothing between
|
|
480
|
+
* the base and the head) or `no_base` (the instance names no base to
|
|
481
|
+
* open against, so no create was tried). Absent on a plain check. */
|
|
482
|
+
unrecovered?: "no_commits" | "no_base";
|
|
483
|
+
}
|
|
446
484
|
| { state: "open"; prNumber: number; url: string; headSha?: string; autoMergeEnabled?: boolean }
|
|
447
485
|
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string };
|
|
448
486
|
|
|
@@ -473,7 +511,12 @@ export type UnitEnding =
|
|
|
473
511
|
| { kind: "merge_ready"; pr: PrRef; reviewRounds: number }
|
|
474
512
|
| { kind: "merge_refused"; pr: PrRef; reason: string; reviewRounds: number }
|
|
475
513
|
| { kind: "round_cap"; maxRounds: number; reviewRounds: number }
|
|
476
|
-
| { kind: "wall_clock_cap"; remainingMs: number; reviewRounds: number }
|
|
514
|
+
| { kind: "wall_clock_cap"; remainingMs: number; reviewRounds: number; spent: ShipBudgetSpent }
|
|
515
|
+
/** The wall clock capped AFTER the coding child opened or updated the pull
|
|
516
|
+
* request: the work stands and only the review is missing, so the ending
|
|
517
|
+
* names the pull request and "review pending" instead of calling the unit a
|
|
518
|
+
* failure — the re-issued attempt starts at the review round (`lastPush`). */
|
|
519
|
+
| { kind: "review_pending"; pr: PrRef; headSha?: string; reviewRounds: number; spent: ShipBudgetSpent }
|
|
477
520
|
| {
|
|
478
521
|
kind: "stopped";
|
|
479
522
|
mode: "soft" | "hard";
|
|
@@ -494,6 +537,12 @@ export type CoordinatorNote =
|
|
|
494
537
|
| { type: "round"; index: number; agent: ChildPreset; outcome: ShipRoundOutcome }
|
|
495
538
|
| { type: "ended"; ending: UnitEnding };
|
|
496
539
|
|
|
540
|
+
/** How the pipeline's budget went, in ms: the coding rounds' (round 0 and the
|
|
541
|
+
* findings steps), the review rounds', and everything else (branching,
|
|
542
|
+
* pr-checks, busy waits, merge polls) — reported on the cap endings so a
|
|
543
|
+
* person can see whether the cap or the child is the problem. */
|
|
544
|
+
export type ShipBudgetSpent = Readonly<Record<"coding" | "review" | "waiting", number>>;
|
|
545
|
+
|
|
497
546
|
export interface UnitPipelineInput {
|
|
498
547
|
unit: { id: string; branch: string };
|
|
499
548
|
repo: string;
|
|
@@ -513,6 +562,11 @@ export interface UnitPipelineInput {
|
|
|
513
562
|
generated: boolean;
|
|
514
563
|
/** Resume at review: an open pull request of ship's own the requester named. */
|
|
515
564
|
resume?: { pr: number; headSha?: string; url?: string };
|
|
565
|
+
/** The head the previous attempt's coding child last pushed (a
|
|
566
|
+
* `review_pending` ending's `headSha`, carried on the unit's row): when the
|
|
567
|
+
* pre-check finds the open pull request still at exactly this head, there is
|
|
568
|
+
* nothing to code and the attempt starts at the review round. */
|
|
569
|
+
lastPush?: string;
|
|
516
570
|
}
|
|
517
571
|
|
|
518
572
|
type Phase =
|
|
@@ -524,7 +578,17 @@ type Phase =
|
|
|
524
578
|
/** `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
579
|
| { at: "wait"; round: RoundRef; runId: string; n: number; until: number }
|
|
526
580
|
| { at: "read"; round: RoundRef; runId: string; n: number; until: number }
|
|
527
|
-
| {
|
|
581
|
+
| {
|
|
582
|
+
at: "pr-check";
|
|
583
|
+
round: RoundRef;
|
|
584
|
+
runId: string;
|
|
585
|
+
childHead?: string;
|
|
586
|
+
finalReply?: string;
|
|
587
|
+
/** The coding child died (`failed` or `interrupted`) after it may have
|
|
588
|
+
* pushed: the pr-check recovers a pushed branch by opening its pull
|
|
589
|
+
* request; with nothing pushed the unit ends with the child's own reason. */
|
|
590
|
+
dead?: "failed" | "interrupted";
|
|
591
|
+
}
|
|
528
592
|
| { at: "merge"; pr: PrRef; headSha: string; n: number; since: number }
|
|
529
593
|
| { at: "merge-sleep"; pr: PrRef; headSha: string; n: number; since: number }
|
|
530
594
|
| { at: "ended" };
|
|
@@ -551,6 +615,8 @@ export interface UnitPipelineState {
|
|
|
551
615
|
readonly findingsRunByRound: Readonly<Record<number, string>>;
|
|
552
616
|
/** The last coding run (round 0's child or a findings step's): its record carries the unit's handoff. */
|
|
553
617
|
readonly lastCodingRunId?: string;
|
|
618
|
+
/** How the budget went so far, accrued as each answer moves the clock. */
|
|
619
|
+
readonly spentMs: ShipBudgetSpent;
|
|
554
620
|
readonly ending?: UnitEnding;
|
|
555
621
|
}
|
|
556
622
|
|
|
@@ -579,6 +645,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
579
645
|
clock: at,
|
|
580
646
|
phase: { at: "pre-check" },
|
|
581
647
|
reviewRounds: 0,
|
|
648
|
+
spentMs: { coding: 0, review: 0, waiting: 0 },
|
|
582
649
|
findingsByRound: {},
|
|
583
650
|
dispositionsByRound: {},
|
|
584
651
|
reviewRunByRound: {},
|
|
@@ -609,8 +676,25 @@ function waitSliceMs(clock: number, until: number): number {
|
|
|
609
676
|
/** The child's budget: its preset's own, clipped to the pipeline's remaining
|
|
610
677
|
* wall clock (agent-ship item 8's clip), never under the two minutes a
|
|
611
678
|
* spawn accepts. */
|
|
612
|
-
function budgetMinutesFor(s: UnitPipelineState,
|
|
613
|
-
|
|
679
|
+
function budgetMinutesFor(s: UnitPipelineState, round: RoundRef): number {
|
|
680
|
+
const headroom = remainingMs(s) - reserveFor(round.kind);
|
|
681
|
+
return Math.max(2, Math.min(s.input.childMinutes[presetOf(round.kind)], Math.floor(headroom / MIN)));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** What a child of each round kind leaves on the pipeline's clock: round 0's
|
|
685
|
+
* coding child the whole loop (two reviews and the merge poll,
|
|
686
|
+
* SHIP_LOOP_RESERVE_MS), a findings child the re-review and the merge poll
|
|
687
|
+
* (SHIP_FIX_RESERVE_MS), a review child nothing — the review is what the
|
|
688
|
+
* reserve was held for. */
|
|
689
|
+
function reserveFor(kind: RoundKind): number {
|
|
690
|
+
switch (kind) {
|
|
691
|
+
case "coding":
|
|
692
|
+
return SHIP_LOOP_RESERVE_MS;
|
|
693
|
+
case "findings":
|
|
694
|
+
return SHIP_FIX_RESERVE_MS;
|
|
695
|
+
case "review":
|
|
696
|
+
return 0;
|
|
697
|
+
}
|
|
614
698
|
}
|
|
615
699
|
|
|
616
700
|
const roundStep = (s: UnitPipelineState, round: RoundRef) => `${s.input.unit.id}/${round.index}/${round.kind}`;
|
|
@@ -668,7 +752,7 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
668
752
|
step: roundStep(s, p.round),
|
|
669
753
|
preset,
|
|
670
754
|
round: p.round,
|
|
671
|
-
budgetMinutes: budgetMinutesFor(s,
|
|
755
|
+
budgetMinutes: budgetMinutesFor(s, p.round),
|
|
672
756
|
brief: briefFor(s, p.round),
|
|
673
757
|
};
|
|
674
758
|
}
|
|
@@ -687,7 +771,11 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
687
771
|
case "read":
|
|
688
772
|
return { type: "read-record", step: `${roundStep(s, p.round)}/read/${p.n}`, runId: p.runId };
|
|
689
773
|
case "pr-check":
|
|
690
|
-
return {
|
|
774
|
+
return {
|
|
775
|
+
type: "pr-check",
|
|
776
|
+
step: `${roundStep(s, p.round)}/pr-check`,
|
|
777
|
+
...(p.dead !== undefined ? { recover: { runId: p.runId } } : {}),
|
|
778
|
+
};
|
|
691
779
|
case "merge":
|
|
692
780
|
return { type: "merge", step: `${unit}/merge/${p.n}`, prNumber: p.pr.number, headSha: p.headSha };
|
|
693
781
|
case "merge-sleep":
|
|
@@ -717,12 +805,26 @@ const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): CoordinatorNote
|
|
|
717
805
|
outcome,
|
|
718
806
|
});
|
|
719
807
|
|
|
808
|
+
/** The cap's ending: `review_pending` when the clock ran out entering a
|
|
809
|
+
* review round with the child's pull request standing — the work shipped and
|
|
810
|
+
* only the review is missing — else the wall-clock cap. Both carry the split. */
|
|
811
|
+
function capEnding(s: UnitPipelineState, round?: RoundRef): UnitEnding {
|
|
812
|
+
if (round?.kind === "review" && s.pr !== undefined)
|
|
813
|
+
return {
|
|
814
|
+
kind: "review_pending",
|
|
815
|
+
pr: s.pr,
|
|
816
|
+
...(s.lastReviewHead !== undefined ? { headSha: s.lastReviewHead } : {}),
|
|
817
|
+
reviewRounds: s.reviewRounds,
|
|
818
|
+
spent: s.spentMs,
|
|
819
|
+
};
|
|
820
|
+
return { kind: "wall_clock_cap", remainingMs: remainingMs(s), reviewRounds: s.reviewRounds, spent: s.spentMs };
|
|
821
|
+
}
|
|
822
|
+
|
|
720
823
|
/** Start a round if the reservation holds (agent-ship item 8's check: a
|
|
721
824
|
* child clipped under the reserve cannot do useful work). */
|
|
722
825
|
function enterRound(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNote[] = []): Transition {
|
|
723
826
|
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);
|
|
827
|
+
if (remaining < SHIP_ROUND_RESERVE_MS) return end(s, capEnding(s, round), notes);
|
|
726
828
|
const reviewRounds = round.kind === "review" ? round.index : s.reviewRounds;
|
|
727
829
|
return { state: { ...s, reviewRounds, phase: { at: "spawn", round, busy: 0 } }, notes };
|
|
728
830
|
}
|
|
@@ -773,17 +875,12 @@ function settleCoding(
|
|
|
773
875
|
},
|
|
774
876
|
[roundNote(round, "stopped")],
|
|
775
877
|
);
|
|
878
|
+
// A failed coding child no longer aborts outright: the pr-check looks at the
|
|
879
|
+
// branch first — a push before the death is recovered as the round's pull
|
|
880
|
+
// request (agent-ship items 10 and 15), and only a branch with
|
|
881
|
+
// nothing on it ends the unit with the child's own reason.
|
|
776
882
|
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
|
-
);
|
|
883
|
+
return { state: { ...next, phase: { at: "pr-check", round, runId, dead: "failed" } }, notes: [] };
|
|
787
884
|
next = {
|
|
788
885
|
...next,
|
|
789
886
|
phase: {
|
|
@@ -919,6 +1016,32 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
919
1016
|
// completed without a pull request of its own, and the unit is done.
|
|
920
1017
|
if (pr.state === "merged") return foundMerged(s, pr, [roundNote(round, "completed")]);
|
|
921
1018
|
if (pr.state === "none") {
|
|
1019
|
+
// A dead child left nothing on the branch to recover: the unit ends with
|
|
1020
|
+
// the child's own reason — never the budget clip.
|
|
1021
|
+
if (phase.dead === "interrupted")
|
|
1022
|
+
return end(s, { kind: "interrupted", round, runId: phase.runId, reviewRounds: s.reviewRounds }, [
|
|
1023
|
+
roundNote(round, "aborted"),
|
|
1024
|
+
]);
|
|
1025
|
+
if (phase.dead === "failed") {
|
|
1026
|
+
// The abort repeats the bot's reason for recovering nothing, and claims
|
|
1027
|
+
// no more than the answer carried.
|
|
1028
|
+
const why =
|
|
1029
|
+
pr.unrecovered === "no_commits"
|
|
1030
|
+
? `nothing heads \`${s.input.unit.branch}\`: no commits were pushed, so there was no work to recover`
|
|
1031
|
+
: pr.unrecovered === "no_base"
|
|
1032
|
+
? `\`${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`
|
|
1033
|
+
: `the pr-check found no pull request heading \`${s.input.unit.branch}\`, so nothing was recovered`;
|
|
1034
|
+
return end(
|
|
1035
|
+
s,
|
|
1036
|
+
{
|
|
1037
|
+
kind: "aborted",
|
|
1038
|
+
reason: `⚠️ The coding child of round ${round.index} (run ${phase.runId}) ended \`failed\` — its run page has the error — and ${why}.`,
|
|
1039
|
+
round,
|
|
1040
|
+
reviewRounds: s.reviewRounds,
|
|
1041
|
+
},
|
|
1042
|
+
[roundNote(round, "aborted")],
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
922
1045
|
const reason =
|
|
923
1046
|
round.index === 0
|
|
924
1047
|
? `⚠️ 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 +1067,25 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
944
1067
|
const codingHead = normalizeHead(head);
|
|
945
1068
|
const reviewedAt = normalizeHead(s.lastReviewHead);
|
|
946
1069
|
if (codingHead !== undefined && reviewedAt !== undefined && sameCommit(codingHead, reviewedAt)) {
|
|
1070
|
+
// A findings child that died before it pushed: the recover pr-check found
|
|
1071
|
+
// the round's own pull request, still at the reviewed head. The unit
|
|
1072
|
+
// ends with the child's own reason — the ship-restart note for a bot
|
|
1073
|
+
// roll, the failure for a failed run — never as the round's inaction.
|
|
1074
|
+
if (phase.dead === "interrupted")
|
|
1075
|
+
return end(next, { kind: "interrupted", round, runId: phase.runId, reviewRounds: next.reviewRounds }, [
|
|
1076
|
+
roundNote(round, "aborted"),
|
|
1077
|
+
]);
|
|
1078
|
+
if (phase.dead === "failed")
|
|
1079
|
+
return end(
|
|
1080
|
+
next,
|
|
1081
|
+
{
|
|
1082
|
+
kind: "aborted",
|
|
1083
|
+
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.`,
|
|
1084
|
+
round,
|
|
1085
|
+
reviewRounds: next.reviewRounds,
|
|
1086
|
+
},
|
|
1087
|
+
[roundNote(round, "aborted")],
|
|
1088
|
+
);
|
|
947
1089
|
const findings = s.findingsByRound[round.index] ?? [];
|
|
948
1090
|
const dispositions = s.dispositionsByRound[round.index] ?? [];
|
|
949
1091
|
const allDeclined =
|
|
@@ -967,6 +1109,17 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
967
1109
|
]);
|
|
968
1110
|
}
|
|
969
1111
|
|
|
1112
|
+
/** Move the clock and charge the elapsed time to the budget's bucket: a phase
|
|
1113
|
+
* inside a coding or findings round is the coding child's time, a review
|
|
1114
|
+
* round's is the review's, everything else — the pre-check, the branch, busy
|
|
1115
|
+
* waits, the merge polls — is waiting. The split rides the cap endings. */
|
|
1116
|
+
function withClock(s: UnitPipelineState, at: number): UnitPipelineState {
|
|
1117
|
+
const delta = Math.max(0, at - s.clock);
|
|
1118
|
+
const round = "round" in s.phase ? s.phase.round : undefined;
|
|
1119
|
+
const bucket = round === undefined ? "waiting" : round.kind === "review" ? "review" : "coding";
|
|
1120
|
+
return { ...s, clock: at, spentMs: { ...s.spentMs, [bucket]: s.spentMs[bucket] + delta } };
|
|
1121
|
+
}
|
|
1122
|
+
|
|
970
1123
|
/**
|
|
971
1124
|
* Feed a step's answer to the machine. An answer for any step but the one the
|
|
972
1125
|
* machine is at — a duplicate `run finished`, a replayed spawn — changes
|
|
@@ -976,7 +1129,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
976
1129
|
const expected = nextAction(s);
|
|
977
1130
|
if (expected.type === "end" || ret.step !== expected.step || ret.type !== expected.type)
|
|
978
1131
|
return { state: s, notes: [] };
|
|
979
|
-
const clocked: UnitPipelineState = "at" in ret ?
|
|
1132
|
+
const clocked: UnitPipelineState = "at" in ret ? withClock(s, ret.at) : s;
|
|
980
1133
|
const p = s.phase;
|
|
981
1134
|
switch (p.at) {
|
|
982
1135
|
case "pre-check": {
|
|
@@ -987,6 +1140,20 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
987
1140
|
// by the coding child and adopted at the round's own pr-check.
|
|
988
1141
|
const r = ret as Extract<StepReturn, { type: "pr-check" }>;
|
|
989
1142
|
if (r.pr.state === "merged") return foundMerged(clocked, r.pr);
|
|
1143
|
+
// The open pull request still heads at the child's own last push (the
|
|
1144
|
+
// previous attempt ended `review_pending`): nothing to code, so the
|
|
1145
|
+
// attempt adopts it and starts at the review round — never a fresh
|
|
1146
|
+
// coding round on an already-shipped pull request.
|
|
1147
|
+
if (r.pr.state === "open") {
|
|
1148
|
+
const head = normalizeHead(r.pr.headSha);
|
|
1149
|
+
const lastPush = normalizeHead(s.input.lastPush);
|
|
1150
|
+
if (head !== undefined && lastPush !== undefined && sameCommit(head, lastPush))
|
|
1151
|
+
return nextReview({
|
|
1152
|
+
...clocked,
|
|
1153
|
+
pr: { number: r.pr.prNumber, url: r.pr.url },
|
|
1154
|
+
lastReviewHead: head,
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
990
1157
|
return { state: { ...clocked, phase: { at: "branch" } }, notes: [] };
|
|
991
1158
|
}
|
|
992
1159
|
case "branch": {
|
|
@@ -1004,7 +1171,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1004
1171
|
case "spawned":
|
|
1005
1172
|
case "alreadySpawned": {
|
|
1006
1173
|
// 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,
|
|
1174
|
+
const until = clocked.clock + budgetMinutesFor(s, p.round) * MIN + WAIT_MARGIN_MS;
|
|
1008
1175
|
const runs =
|
|
1009
1176
|
p.round.kind === "review"
|
|
1010
1177
|
? { reviewRunByRound: { ...s.reviewRunByRound, [p.round.index]: r.runId } }
|
|
@@ -1022,12 +1189,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1022
1189
|
case "busy": {
|
|
1023
1190
|
// Another run holds the unit's thread: wait for its end, then ask
|
|
1024
1191
|
// 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
|
-
});
|
|
1192
|
+
if (remainingMs(clocked) < SHIP_ROUND_RESERVE_MS) return end(clocked, capEnding(clocked, p.round));
|
|
1031
1193
|
return {
|
|
1032
1194
|
state: {
|
|
1033
1195
|
...clocked,
|
|
@@ -1076,10 +1238,19 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
1076
1238
|
},
|
|
1077
1239
|
notes: [],
|
|
1078
1240
|
};
|
|
1079
|
-
if (r.run.status === "interrupted")
|
|
1241
|
+
if (r.run.status === "interrupted") {
|
|
1242
|
+
// A dead CODING child may have pushed before the ledger closed it: the
|
|
1243
|
+
// pr-check recovers the branch. A review child has nothing on the
|
|
1244
|
+
// branch to recover, so its interruption still ends the unit at once.
|
|
1245
|
+
if (p.round.kind !== "review")
|
|
1246
|
+
return {
|
|
1247
|
+
state: { ...clocked, phase: { at: "pr-check", round: p.round, runId: p.runId, dead: "interrupted" } },
|
|
1248
|
+
notes: [],
|
|
1249
|
+
};
|
|
1080
1250
|
return end(clocked, { kind: "interrupted", round: p.round, runId: p.runId, reviewRounds: s.reviewRounds }, [
|
|
1081
1251
|
roundNote(p.round, "aborted"),
|
|
1082
1252
|
]);
|
|
1253
|
+
}
|
|
1083
1254
|
return p.round.kind === "review"
|
|
1084
1255
|
? settleReview(clocked, p.round, r.run)
|
|
1085
1256
|
: settleCoding(clocked, p.round, p.runId, r.run);
|
|
@@ -1135,6 +1306,12 @@ function dispositionFor(s: UnitPipelineState, finding: Finding, round: number):
|
|
|
1135
1306
|
return undefined;
|
|
1136
1307
|
}
|
|
1137
1308
|
|
|
1309
|
+
/** How the budget went, in the card's words: coding, review, waiting minutes. */
|
|
1310
|
+
function budgetSplitLine(spent: ShipBudgetSpent, maxMinutes: number): string {
|
|
1311
|
+
const min = (ms: number) => Math.round(ms / MIN);
|
|
1312
|
+
return `Budget split (${maxMinutes} min): coding ${min(spent.coding)} min, review ${min(spent.review)} min, waiting ${min(spent.waiting)} min.`;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1138
1315
|
/** The cap report's declined-vs-unaddressed split over the last review round's findings. */
|
|
1139
1316
|
function splitReport(s: UnitPipelineState): string {
|
|
1140
1317
|
if (s.reviewRounds === 0) return "No review round ran before the cap — there are no findings to report.";
|
|
@@ -1222,9 +1399,16 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1222
1399
|
case "wall_clock_cap":
|
|
1223
1400
|
return join([
|
|
1224
1401
|
`🧢 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}`,
|
|
1402
|
+
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
1225
1403
|
splitReport(s),
|
|
1226
1404
|
reissue,
|
|
1227
1405
|
]);
|
|
1406
|
+
case "review_pending":
|
|
1407
|
+
return join([
|
|
1408
|
+
`⏳ 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.`,
|
|
1409
|
+
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
1410
|
+
reissue,
|
|
1411
|
+
]);
|
|
1228
1412
|
case "stopped":
|
|
1229
1413
|
return join([
|
|
1230
1414
|
`${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 {
|
|
@@ -93,6 +93,30 @@ export function leftBehindOf(measured: WorktreeCleanliness): LeftBehind | undefi
|
|
|
93
93
|
return { uncommittedChanges: measured.changes, unpushedCommits: measured.unpushed };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** What an eviction records about the tree it removed
|
|
97
|
+
* (docs/reference/specs/resident-repos.md item 17: dirt never keeps a tree,
|
|
98
|
+
* and nothing is discarded silently), written on the binding beside the
|
|
99
|
+
* cause: the counts when the probes ran and found something, or the probe's
|
|
100
|
+
* first error line when git could not read the tree — nothing readable was
|
|
101
|
+
* kept, but the record must not read as clean. */
|
|
102
|
+
export type EvictedTree = { leftBehind: LeftBehind } | { unmeasured: string };
|
|
103
|
+
|
|
104
|
+
/** The measurement as the eviction's record: `undefined` for a clean tree or
|
|
105
|
+
* one already gone with the disk (nothing was discarded). */
|
|
106
|
+
export function evictedTreeOf(measured: WorktreeCleanliness): EvictedTree | undefined {
|
|
107
|
+
const leftBehind = leftBehindOf(measured);
|
|
108
|
+
if (leftBehind) return { leftBehind };
|
|
109
|
+
if (!measured.clean) return { unmeasured: measured.reason ?? "clean-check failed" };
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The resident's log line for what an eviction discarded. */
|
|
114
|
+
export function evictedTreeSentence(tree: EvictedTree): string {
|
|
115
|
+
if ("leftBehind" in tree)
|
|
116
|
+
return `left behind ${tree.leftBehind.uncommittedChanges} uncommitted change(s) and ${tree.leftBehind.unpushedCommits} unpushed commit(s), discarded with the tree`;
|
|
117
|
+
return `the tree could not be measured before its removal (${tree.unmeasured})`;
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
/** The bot's word for a discarded tree, in the release log and the run's
|
|
97
121
|
* record: what was left, why it is gone, what to do instead. */
|
|
98
122
|
export function leftBehindSentence(left: LeftBehind): string {
|
|
@@ -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" };
|
|
@@ -311,17 +311,18 @@ export interface DiskEvictionCandidate {
|
|
|
311
311
|
|
|
312
312
|
/** Why a tree was kept under disk pressure. `other` is the caller's fallback
|
|
313
313
|
* for a keep decided outside `orderEvictionCandidates` (a binding that moved
|
|
314
|
-
* during the check); free text never becomes a token.
|
|
315
|
-
|
|
314
|
+
* during the check); free text never becomes a token. No token names dirt:
|
|
315
|
+
* a tree is never kept for what it holds (a run starts from a clean tree),
|
|
316
|
+
* the Worker records what it removes instead. */
|
|
317
|
+
export type DiskKeepWhy = "busy" | "default-ref" | "recent" | "requesting" | "other";
|
|
316
318
|
|
|
317
319
|
/** Order the live trees for eviction under pressure and name every one that is
|
|
318
320
|
* kept: the requesting thread itself, a busy tree, the default branch (the
|
|
319
321
|
* one most likely re-attached — the same rule as `reclaimDecision`), and a
|
|
320
322
|
* tree attached within `DISK_EVICT_MIN_IDLE_MS` are never candidates; the
|
|
321
|
-
* rest are ordered coldest first (oldest `lastAttachAt`, ties on key).
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* or unreadable one (`dirty`), exactly like the sweep. */
|
|
323
|
+
* rest are ordered coldest first (oldest `lastAttachAt`, ties on key). What
|
|
324
|
+
* each tree holds is not asked — the Worker measures it as the thread user
|
|
325
|
+
* right before the eviction, for the record only. */
|
|
325
326
|
export function orderEvictionCandidates(input: {
|
|
326
327
|
candidates: readonly DiskEvictionCandidate[];
|
|
327
328
|
now: number;
|