@coreplane/switchboard 1.242.0 → 1.244.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/config/config.example.yaml +20 -1
- package/dist/assets/deploy/cloudflare-memory/worker.ts +234 -29
- package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +483 -75
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +3 -3
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +31 -35
- package/dist/assets/src/config/profile.ts +68 -3
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/types.ts +10 -0
- package/dist/assets/src/core/budgets.ts +349 -0
- package/dist/assets/src/core/coordinator/contract.ts +32 -0
- package/dist/assets/src/core/coordinator/driver.ts +75 -22
- package/dist/assets/src/core/costs.ts +9 -76
- package/dist/assets/src/core/delivery.ts +1 -1
- package/dist/assets/src/core/modelPricing.ts +212 -0
- package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
- package/dist/assets/src/core/reviewVerdict.ts +7 -0
- package/dist/assets/src/core/runEvents.ts +72 -5
- package/dist/assets/src/core/runFriction.ts +3 -1
- package/dist/assets/src/core/runRecord.ts +78 -0
- package/dist/assets/src/core/runUsage.ts +158 -47
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +2 -1
- package/dist/assets/src/core/ship/coordinator.ts +312 -97
- package/dist/assets/src/core/ship/handoff.ts +9 -0
- package/dist/assets/src/core/ship/renewal.ts +155 -0
- package/dist/assets/src/core/trace/attrs.ts +11 -0
- package/dist/assets/src/core/trace/types.ts +4 -3
- package/dist/assets/src/execution/bashTimeout.ts +8 -5
- package/dist/assets/src/execution/residentRefresh.ts +52 -7
- package/dist/assets/src/execution/residentSteps.ts +1 -1
- package/dist/assets/src/execution/residentText.ts +8 -3
- package/dist/assets/src/mcp/registry.ts +3 -0
- package/dist/assets/web/dist/.vite/manifest.json +432 -429
- package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DTMBgnIW.js → ResidentDetailPage-C5USV_8P.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-ELoXHlFt.js → StatusDot-_Q_bmReh.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BoeFwYP2.js → Tooltip-Cafsgd4m.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
- package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BU5UivXC.js → dist-BtPs2AkQ.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
- package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
- package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +1 -0
- package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
- package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
- package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
- package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
- package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
- package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
- package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
- package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
- package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-g7ml86LM.js → sseReplay-DaRtc7nI.js} +5 -5
- package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
- package/dist/cli.js +5843 -2246
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CtmKhhOF.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CaDvXhzJ.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DXD2gLJk.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-B2fX10aW.css +0 -1
- package/dist/assets/web/dist/assets/main-D4EA1g6n.js +0 -28
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Every wall clock in Switchboard as one table (docs/decisions/0046; docs/reference/specs/harness-pi.md
|
|
2
|
+
// item 15; docs/reference/specs/agent-ship.md item 8). A budget is a LEASE a
|
|
3
|
+
// parent carves from its own remainder, never a constant a file holds on its
|
|
4
|
+
// own: a preset ASKS for a lease and does useful work above a FLOOR; a parent
|
|
5
|
+
// that runs a loop holds back a RESERVE, derived from the floors and the
|
|
6
|
+
// provisioning of every round that must still follow, plus the merge wait's
|
|
7
|
+
// floor; and the lease covers everything the run does, the loop, the write-up
|
|
8
|
+
// and the post-step, each an ALLOWANCE named here. The registry reads its
|
|
9
|
+
// `maxMinutes` and `maxTurns` from this module and `budgets.check.test.ts`
|
|
10
|
+
// asserts the fits, so a number that breaks another's assumption is a red
|
|
11
|
+
// build. `carve` is called by the ship coordinator for every round (agent-ship
|
|
12
|
+
// item 8), `fit` by the config validator at load and by the fork over a clipped
|
|
13
|
+
// request, and `carveChildOfParent` by the conductor's spawn.
|
|
14
|
+
//
|
|
15
|
+
// Deliberately free of node: imports and of the agent registry, so the
|
|
16
|
+
// Workflow-driven coordinator and the deploy Workers can bundle it — the
|
|
17
|
+
// dependency runs registry → budgets, never the reverse.
|
|
18
|
+
|
|
19
|
+
export const MINUTE_MS = 60_000;
|
|
20
|
+
|
|
21
|
+
/** One calendar day in milliseconds: the unit the daily cost and delivery ranges
|
|
22
|
+
* step by (`dayOf`, the day count of a range). A day is not a lease, but it is
|
|
23
|
+
* a duration, and every duration is read from this table rather than written
|
|
24
|
+
* as a literal where it is used. */
|
|
25
|
+
export const DAY_MS = 24 * 60 * MINUTE_MS;
|
|
26
|
+
|
|
27
|
+
/** How long the confirmation a routed write is offered as stays pending
|
|
28
|
+
* (docs/decisions/0044; docs/reference/specs/routing-and-config.md item 25):
|
|
29
|
+
* the connect ticket's ten minutes. The bot passes it to the config object,
|
|
30
|
+
* which stamps the expiry on its own clock. */
|
|
31
|
+
export const CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
|
|
32
|
+
|
|
33
|
+
/** The presets that run the tool loop, and the one pipeline preset. */
|
|
34
|
+
export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
|
|
35
|
+
export type LoopPreset = (typeof LOOP_PRESETS)[number];
|
|
36
|
+
export type Preset = LoopPreset | "ship";
|
|
37
|
+
|
|
38
|
+
/** What each preset asks for when nothing above it is tighter, in minutes. The
|
|
39
|
+
* pipeline's (`ship`) is the wall clock one segment of its loop runs under;
|
|
40
|
+
* a deployment's `ship.maxMinutes` replaces it, held to `fit` below. */
|
|
41
|
+
export const ASKS: Readonly<Record<Preset, number>> = {
|
|
42
|
+
general: 5,
|
|
43
|
+
coding: 45,
|
|
44
|
+
review: 25,
|
|
45
|
+
ship: 120,
|
|
46
|
+
research: 8,
|
|
47
|
+
explore: 120,
|
|
48
|
+
conductor: 120,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The rounds a ship loop is made of. `fix` is a coding child handed the
|
|
52
|
+
* review's findings; `merge` is the runner's wait on the guards. */
|
|
53
|
+
export type RoundKind = "coding" | "review" | "fix" | "merge";
|
|
54
|
+
|
|
55
|
+
/** The least lease in which a round does useful work, in minutes. A carve that
|
|
56
|
+
* falls under the floor is refused rather than dispatched: a two-minute
|
|
57
|
+
* review or fix costs an attach and a model turn and finishes nothing.
|
|
58
|
+
* Review's is the ledger's 90th percentile of completed reviews (5.1 min over
|
|
59
|
+
* 181); coding's and the merge wait's are guesses until the ledger says. */
|
|
60
|
+
export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
|
|
61
|
+
general: 2,
|
|
62
|
+
coding: 10,
|
|
63
|
+
review: 5,
|
|
64
|
+
research: 3,
|
|
65
|
+
explore: 15,
|
|
66
|
+
conductor: 15,
|
|
67
|
+
};
|
|
68
|
+
export const FLOORS: Readonly<Record<RoundKind, number>> = {
|
|
69
|
+
coding: PRESET_FLOORS.coding,
|
|
70
|
+
fix: PRESET_FLOORS.coding,
|
|
71
|
+
review: PRESET_FLOORS.review,
|
|
72
|
+
merge: 10,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** The merge wait's own ask: how long the runner waits on the guards at most
|
|
76
|
+
* when the remainder allows it. */
|
|
77
|
+
export const MERGE_WAIT_ASK_MINUTES = 60;
|
|
78
|
+
|
|
79
|
+
/** The ship runner's waits, in minutes: the margin a child's wait allows past
|
|
80
|
+
* its budget, the slice a wait is asked in, the merge door's re-ask cadence,
|
|
81
|
+
* and the pause before a busy spawn is asked again. */
|
|
82
|
+
export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes: 5, busyRetryMinutes: 2 } as const;
|
|
83
|
+
|
|
84
|
+
/** The named amounts a lease holds back, in minutes. Each stands for a step
|
|
85
|
+
* every run or round pays: `provision` is attach and restore before the
|
|
86
|
+
* harness's clock starts; `writeUp` is the final answer after the loop ends;
|
|
87
|
+
* `commandWriteUp` is what the last command leaves for that answer; `execCall`
|
|
88
|
+
* is the exec client's wait past a command's own budget; `bearerGrace` is how
|
|
89
|
+
* far past the lease the model bearer stays valid for the last call's tail. */
|
|
90
|
+
export const ALLOWANCES = {
|
|
91
|
+
provision: 3,
|
|
92
|
+
writeUp: 3,
|
|
93
|
+
commandWriteUp: 1,
|
|
94
|
+
execCall: 0.5,
|
|
95
|
+
bearerGrace: 1,
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
/** The post-step turn a preset runs after its loop, in minutes: the coding
|
|
99
|
+
* run's description turn, the review's verdict turn, none for the rest. */
|
|
100
|
+
export const POST_STEP_MINUTES: Readonly<Record<Preset, number>> = {
|
|
101
|
+
coding: 5,
|
|
102
|
+
review: 3,
|
|
103
|
+
general: 0,
|
|
104
|
+
research: 0,
|
|
105
|
+
explore: 0,
|
|
106
|
+
conductor: 0,
|
|
107
|
+
ship: 0,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** The post-step allowance of a preset named by its def's `name`: a name
|
|
111
|
+
* outside the table (a test's, a command run's) runs no post-step. */
|
|
112
|
+
export function postStepMinutes(preset: string): number {
|
|
113
|
+
return (POST_STEP_MINUTES as Readonly<Record<string, number>>)[preset] ?? 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The post-step turn's lease, in minutes (fractional when the remainder is:
|
|
117
|
+
* the harness turns it back into ms): the preset's allowance, or the lease's
|
|
118
|
+
* remainder when that is less — never under a minute, so a turn the loop's
|
|
119
|
+
* write-up crowded still gets its one chance. The bearer's grace covers that
|
|
120
|
+
* floor for a turn that starts at or before the lease's end; a turn that
|
|
121
|
+
* starts later runs on a bearer that may expire under it, and its refused
|
|
122
|
+
* call fails soft — the run's answer already stands. Without a remainder
|
|
123
|
+
* (no harness session says) the allowance stands. */
|
|
124
|
+
export function postStepLease(preset: string, remainingMs: number | undefined): number {
|
|
125
|
+
const allowance = postStepMinutes(preset);
|
|
126
|
+
if (remainingMs === undefined) return allowance;
|
|
127
|
+
return Math.min(allowance, Math.max(1, remainingMs / MINUTE_MS));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A follow-up turn's lease, in ms: the lesser of what it asks and what the
|
|
131
|
+
* run's lease still holds, never under a minute — the grace covers that floor
|
|
132
|
+
* when the turn starts by the lease's end; later, the bearer may expire under
|
|
133
|
+
* the turn and its call is refused, failing soft. */
|
|
134
|
+
export function turnLeaseMs(askMinutes: number, remainingMs: number): number {
|
|
135
|
+
return Math.min(askMinutes * MINUTE_MS, Math.max(MINUTE_MS, remainingMs));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The wrap-up warning's place before the loop's end: three minutes, or a
|
|
139
|
+
* quarter of the loop when the loop is shorter than twelve. */
|
|
140
|
+
export const WRAP_UP_WARNING = { minutes: 3, fraction: 0.25 } as const;
|
|
141
|
+
|
|
142
|
+
/** The clocks a harness keeps for one lease (docs/reference/specs/harness-pi.md
|
|
143
|
+
* items 6 and 15). The lease ENDS at `deadline`; the LOOP ENDS at `loopEnd`,
|
|
144
|
+
* the write-up allowance and the preset's post-step earlier, so both run
|
|
145
|
+
* inside the lease; the WARNING is steered at `warnAt`; the write-up is
|
|
146
|
+
* BOUNDED by `finaleMs`. A follow-up turn on the session (`kind: "turn"`)
|
|
147
|
+
* holds nothing back — its deliverable is a tool call, not a write-up — and
|
|
148
|
+
* its loop ends at its deadline. A lease shorter than its hold-back has no
|
|
149
|
+
* loop time: the loop ends at its start, never before it. */
|
|
150
|
+
export interface LoopClock {
|
|
151
|
+
startedAt: number;
|
|
152
|
+
deadline: number;
|
|
153
|
+
loopEnd: number;
|
|
154
|
+
warnAt: number;
|
|
155
|
+
finaleMs: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function loopClock(
|
|
159
|
+
startedAt: number,
|
|
160
|
+
remainingMs: number,
|
|
161
|
+
preset: string,
|
|
162
|
+
kind: "loop" | "turn" = "loop",
|
|
163
|
+
): LoopClock {
|
|
164
|
+
const deadline = startedAt + remainingMs;
|
|
165
|
+
const holdBackMs = kind === "loop" ? (ALLOWANCES.writeUp + postStepMinutes(preset)) * MINUTE_MS : 0;
|
|
166
|
+
const loopEnd = Math.max(startedAt, deadline - holdBackMs);
|
|
167
|
+
const warnAt =
|
|
168
|
+
loopEnd - Math.min(WRAP_UP_WARNING.minutes * MINUTE_MS, (loopEnd - startedAt) * WRAP_UP_WARNING.fraction);
|
|
169
|
+
return { startedAt, deadline, loopEnd, warnAt, finaleMs: ALLOWANCES.writeUp * MINUTE_MS };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** When a run's model-proxy bearer expires: the lease's end plus the grace
|
|
173
|
+
* (docs/reference/specs/model-proxy.md item 2). */
|
|
174
|
+
export function bearerExpiresAt(leaseEndsAt: number): number {
|
|
175
|
+
return leaseEndsAt + ALLOWANCES.bearerGrace * MINUTE_MS;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The bearer's expiry as the mint at provisioning sets it, before the lease
|
|
179
|
+
* has started: the provisioning allowance, the lease and the grace — replaced
|
|
180
|
+
* by `bearerExpiresAt` the moment the harness starts the lease. */
|
|
181
|
+
export function provisionalBearerExpiresAt(now: number, leaseMinutes: number): number {
|
|
182
|
+
return now + (ALLOWANCES.provision + leaseMinutes + ALLOWANCES.bearerGrace) * MINUTE_MS;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The per-command bash budget's rows: the default when a call names none, the
|
|
186
|
+
* ceiling a call may raise it to, and the floor under which a number is a
|
|
187
|
+
* typo (docs/reference/specs/execution.md item 11). */
|
|
188
|
+
export const BASH_COMMAND = { defaultMinutes: 5, maxMinutes: 20, minMs: 1_000 } as const;
|
|
189
|
+
|
|
190
|
+
/** The pace that marks a run as looping rather than working: a model turn
|
|
191
|
+
* every ten seconds, sustained for the whole wall clock. A busy run takes
|
|
192
|
+
* 20–40 s a turn (a model think plus a tool call), so a run that averages six
|
|
193
|
+
* a minute from start to end is re-issuing calls, not making progress — and
|
|
194
|
+
* its turn cap ends it before the wall clock would, with a write-up that
|
|
195
|
+
* says so (docs/reference/specs/harness-pi.md item 15). */
|
|
196
|
+
export const RUNAWAY_TURNS_PER_MINUTE = 6;
|
|
197
|
+
|
|
198
|
+
/** The turn cap a wall clock implies: `maxMinutes × RUNAWAY_TURNS_PER_MINUTE`.
|
|
199
|
+
* Every preset that runs the loop derives its `maxTurns` from this, so the
|
|
200
|
+
* cap is never a number a good run reaches — the minutes are the budget. */
|
|
201
|
+
export function runawayTurnCap(maxMinutes: number): number {
|
|
202
|
+
return maxMinutes * RUNAWAY_TURNS_PER_MINUTE;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A pipeline's loop as the config allows it: `maxRounds` counts review rounds. */
|
|
206
|
+
export interface Loop {
|
|
207
|
+
maxRounds: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A round's position in its loop: its kind and its index, so the reserve is
|
|
211
|
+
* the rounds after that index, never a table keyed by kind alone. */
|
|
212
|
+
export interface RoundPosition {
|
|
213
|
+
kind: RoundKind;
|
|
214
|
+
index: number;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The rounds of a loop in order: the coding round, then a review and, after
|
|
218
|
+
* every review but the last, a fix, then the merge wait. */
|
|
219
|
+
export function loopRounds(loop: Loop): RoundKind[] {
|
|
220
|
+
const rounds: RoundKind[] = ["coding"];
|
|
221
|
+
for (let i = 0; i < loop.maxRounds; i++) {
|
|
222
|
+
rounds.push("review");
|
|
223
|
+
if (i < loop.maxRounds - 1) rounds.push("fix");
|
|
224
|
+
}
|
|
225
|
+
rounds.push("merge");
|
|
226
|
+
return rounds;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Where a round sits in `loopRounds`: the coding round first; review round
|
|
230
|
+
* `n` (counted from 1) and the fix that follows it at `2n − 1` and `2n`; the
|
|
231
|
+
* merge wait last. The ship coordinator numbers its rounds this way, so the
|
|
232
|
+
* reserve it carves with is the one the fit assumed. */
|
|
233
|
+
export function loopPosition(loop: Loop, kind: RoundKind, n = 0): number {
|
|
234
|
+
switch (kind) {
|
|
235
|
+
case "coding":
|
|
236
|
+
return 0;
|
|
237
|
+
case "review":
|
|
238
|
+
return 2 * n - 1;
|
|
239
|
+
case "fix":
|
|
240
|
+
return 2 * n;
|
|
241
|
+
case "merge":
|
|
242
|
+
return 2 * loop.maxRounds;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A child spawned outside any loop (a conductor's): the parent's whole
|
|
247
|
+
* remainder in whole minutes, refused under the child preset's floor — a
|
|
248
|
+
* child under its floor costs a thread and a model turn and finishes nothing. */
|
|
249
|
+
export function carveChildOfParent(
|
|
250
|
+
remainingMs: number,
|
|
251
|
+
preset: LoopPreset,
|
|
252
|
+
): { kind: "carved"; minutes: number } | { kind: "refused"; reason: "under floor"; minutes: number; floor: number } {
|
|
253
|
+
const minutes = Math.max(0, Math.floor(remainingMs / MINUTE_MS));
|
|
254
|
+
const floor = PRESET_FLOORS[preset];
|
|
255
|
+
if (minutes < floor) return { kind: "refused", reason: "under floor", minutes, floor };
|
|
256
|
+
return { kind: "carved", minutes };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The minutes a round holds back for what must follow it in the loop: the
|
|
260
|
+
* floor plus provisioning of every later review and fix, and the merge
|
|
261
|
+
* wait's floor. A merge holds nothing back; a round with no loop (a
|
|
262
|
+
* conductor's child) holds nothing back either. */
|
|
263
|
+
export function reserveMinutes(
|
|
264
|
+
round: RoundPosition,
|
|
265
|
+
loop: Loop | undefined,
|
|
266
|
+
floors: Readonly<Record<RoundKind, number>> = FLOORS,
|
|
267
|
+
): number {
|
|
268
|
+
if (!loop) return 0;
|
|
269
|
+
let reserve = 0;
|
|
270
|
+
for (const kind of loopRounds(loop).slice(round.index + 1)) {
|
|
271
|
+
reserve += kind === "merge" ? floors.merge : floors[kind] + ALLOWANCES.provision;
|
|
272
|
+
}
|
|
273
|
+
return reserve;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** What a round asks for: the preset's ask for a coding or fix round, the
|
|
277
|
+
* review's for a review, the merge wait's own for the merge. */
|
|
278
|
+
export function roundAskMinutes(kind: RoundKind): number {
|
|
279
|
+
switch (kind) {
|
|
280
|
+
case "coding":
|
|
281
|
+
case "fix":
|
|
282
|
+
return ASKS.coding;
|
|
283
|
+
case "review":
|
|
284
|
+
return ASKS.review;
|
|
285
|
+
case "merge":
|
|
286
|
+
return MERGE_WAIT_ASK_MINUTES;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** A carve's result: the minutes with what bounded them (`ask` when the round
|
|
291
|
+
* got its whole ask, `parent` when the remainder minus the reserve was
|
|
292
|
+
* tighter) and what the parent holds back, or a refusal when the minutes the
|
|
293
|
+
* remainder leaves fall under the round's floor. */
|
|
294
|
+
export type Carve =
|
|
295
|
+
| { kind: "carved"; minutes: number; boundedBy: "ask" | "parent"; holds: number }
|
|
296
|
+
| { kind: "refused"; reason: "under floor"; minutes: number; floor: number; holds: number };
|
|
297
|
+
|
|
298
|
+
/** The only place a round's minutes are computed: the parent's remainder in
|
|
299
|
+
* whole minutes minus the round's reserve, capped at the round's ask, refused
|
|
300
|
+
* under the round's floor. `loop` is undefined for a child outside any loop. */
|
|
301
|
+
export function carve(remainingMs: number, round: RoundPosition, loop: Loop | undefined): Carve {
|
|
302
|
+
const holds = reserveMinutes(round, loop);
|
|
303
|
+
const ask = roundAskMinutes(round.kind);
|
|
304
|
+
const headroom = Math.floor(remainingMs / MINUTE_MS) - holds;
|
|
305
|
+
const minutes = Math.min(ask, headroom);
|
|
306
|
+
const floor = FLOORS[round.kind];
|
|
307
|
+
// A refusal reports what the remainder left, never a negative number.
|
|
308
|
+
if (minutes < floor) return { kind: "refused", reason: "under floor", minutes: Math.max(0, minutes), floor, holds };
|
|
309
|
+
return { kind: "carved", minutes, boundedBy: minutes === ask ? "ask" : "parent", holds };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** A pipeline's wall clock and its loop: what `fit` judges. */
|
|
313
|
+
export interface Pipeline extends Loop {
|
|
314
|
+
maxMinutes: number;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The fit: a pipeline holds its first child at its ask and every later round
|
|
318
|
+
* at its floor — `provision + ask(coding) + reserve(coding, loop) ≤
|
|
319
|
+
* maxMinutes`. Asserted at verify over the registry, at config load over the
|
|
320
|
+
* deployment's `ship` block (`validateShip`), and at the fork over a request a
|
|
321
|
+
* boundary or a `budget:` directive clipped. `need` is the sum a refusal names. */
|
|
322
|
+
export function fit(pipeline: Pipeline): { ok: boolean; need: number; have: number } {
|
|
323
|
+
const need = ALLOWANCES.provision + ASKS.coding + reserveMinutes({ kind: "coding", index: 0 }, pipeline);
|
|
324
|
+
return { ok: pipeline.maxMinutes >= need, need, have: pipeline.maxMinutes };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---- the grant (decision 0046, Renewal: the grant decides, the lease continues) ----
|
|
328
|
+
|
|
329
|
+
/** What the request authorizes for the whole problem beyond one lease: a count
|
|
330
|
+
* of renewals and a cost cap in dollars. A lease is sized by the infrastructure
|
|
331
|
+
* that must outlive it; the grant is sized by the person — a scope's word or
|
|
332
|
+
* the request's `renewals:` directive — and the runner spends it one segment at
|
|
333
|
+
* a time. Zero renewals and no cap by default, so nothing renews until someone
|
|
334
|
+
* says so; `costCapUsd` absent is no cap, never zero. */
|
|
335
|
+
export interface Grant {
|
|
336
|
+
renewals: number;
|
|
337
|
+
costCapUsd?: number;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Which layer's word the grant is: the request's directive, the user's scope,
|
|
341
|
+
* the channel's, or the org's `ship.grant` — the default counts as the org's. */
|
|
342
|
+
export type GrantSource = "org" | "channel" | "user" | "run";
|
|
343
|
+
|
|
344
|
+
export const DEFAULT_GRANT: Grant = { renewals: 0 };
|
|
345
|
+
|
|
346
|
+
/** The most renewals one request may carry: thirteen segments of the ship
|
|
347
|
+
* preset's ask are a day, the longest problem a person hands over in one
|
|
348
|
+
* message before a plan should carry it. */
|
|
349
|
+
export const GRANT_RENEWALS_MAX = 12;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// parent record the spawn route reads the requester from, and the names the
|
|
9
9
|
// routes decide on.
|
|
10
10
|
//
|
|
11
|
+
import type { Grant, GrantSource } from "../budgets.js";
|
|
11
12
|
import type { AddressSeverity, AddressSeveritySource } from "../ship/coordinator.js";
|
|
12
13
|
|
|
13
14
|
// A coordinator is a Workflow instance in the shim Worker whose children are
|
|
@@ -181,6 +182,13 @@ export interface CoordinatorInstance {
|
|
|
181
182
|
* the machine reads one value; absent reads as the default (`minor`, org). */
|
|
182
183
|
addressSeverity?: AddressSeverity;
|
|
183
184
|
addressSeveritySource?: AddressSeveritySource;
|
|
185
|
+
/** The grant (decision 0046, the renewable lease): the renewals and cost cap
|
|
186
|
+
* the request carries, resolved once by the ship fork (directive count >
|
|
187
|
+
* user > channel > org) and written here beside `merge`; absent reads as
|
|
188
|
+
* zero renewals and no cap, the org's. Nothing renews until the renewal
|
|
189
|
+
* decision reads it. */
|
|
190
|
+
grant?: Grant;
|
|
191
|
+
grantSource?: GrantSource;
|
|
184
192
|
/** The pipeline's caps as the profile gate clipped them: the rounds cap and the wall clock per unit. */
|
|
185
193
|
caps?: { maxRounds: number; maxMinutes: number };
|
|
186
194
|
/** The status card in the requesting thread, when the channel has one — what
|
|
@@ -195,6 +203,13 @@ export interface CoordinatorInstance {
|
|
|
195
203
|
attempt?: number;
|
|
196
204
|
}
|
|
197
205
|
|
|
206
|
+
export interface UnitSegment {
|
|
207
|
+
index: number;
|
|
208
|
+
from?: string;
|
|
209
|
+
runId?: string;
|
|
210
|
+
at: number;
|
|
211
|
+
}
|
|
212
|
+
|
|
198
213
|
/** One unit of the plan an instance runs (a task string is a generated plan of
|
|
199
214
|
* one unit, `U1`): its branch, the units it waits on, and — as the runner
|
|
200
215
|
* reaches it — its thread, its pull request, the round boundaries the card
|
|
@@ -229,6 +244,13 @@ export interface CoordinatorUnit {
|
|
|
229
244
|
* opened or updated): the next attempt's pre-check starts at the review
|
|
230
245
|
* round when the open pull request still heads exactly here. */
|
|
231
246
|
lastPush?: string;
|
|
247
|
+
/** The renewals the unit spent (decision 0046, Renewal): one row per segment
|
|
248
|
+
* the grant opened after the first, keyed by the segment's index — written
|
|
249
|
+
* by `unit-end` on a `continued` ending before the segment runs, so a runner
|
|
250
|
+
* reclaimed between a segment's end and its renewal finds the row and never
|
|
251
|
+
* renews the same segment twice. `from` is the sha the segment continues
|
|
252
|
+
* from, `runId` the coding run whose write-up briefs it. */
|
|
253
|
+
segments?: UnitSegment[];
|
|
232
254
|
/** The round boundaries the coordinator reported, oldest first (the `ship_round` vocabulary). */
|
|
233
255
|
rounds: Array<{ index: number; agent: string; outcome: string; at: number }>;
|
|
234
256
|
/** How the unit ended: the ending's kind and the thread's report, when it has. */
|
|
@@ -281,6 +303,15 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
|
|
|
281
303
|
}
|
|
282
304
|
|
|
283
305
|
/** Structural check on a unit row from outside the process. */
|
|
306
|
+
const isSegment = (v: unknown): boolean =>
|
|
307
|
+
isObject(v) &&
|
|
308
|
+
typeof v.index === "number" &&
|
|
309
|
+
Number.isInteger(v.index) &&
|
|
310
|
+
v.index >= 2 &&
|
|
311
|
+
(v.from === undefined || isText(v.from)) &&
|
|
312
|
+
(v.runId === undefined || isText(v.runId)) &&
|
|
313
|
+
typeof v.at === "number";
|
|
314
|
+
|
|
284
315
|
export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
285
316
|
if (!isObject(v)) return false;
|
|
286
317
|
const r = v;
|
|
@@ -293,6 +324,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
293
324
|
if (r.pr !== undefined && !isPr(r.pr)) return false;
|
|
294
325
|
if (r.resume !== undefined && !isResume(r.resume)) return false;
|
|
295
326
|
if (r.lastPush !== undefined && !isText(r.lastPush)) return false;
|
|
327
|
+
if (r.segments !== undefined && (!Array.isArray(r.segments) || !r.segments.every(isSegment))) return false;
|
|
296
328
|
if (
|
|
297
329
|
!Array.isArray(r.rounds) ||
|
|
298
330
|
r.rounds.length > MAX_ROUNDS ||
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
// plan's re-issue. A task string's ship branch waits for a person. Node-free:
|
|
34
34
|
// the shim Worker imports this by relative path.
|
|
35
35
|
|
|
36
|
+
import { DEFAULT_GRANT, GRANT_RENEWALS_MAX, type Grant, type GrantSource } from "../budgets.js";
|
|
36
37
|
import {
|
|
37
38
|
applyReturn,
|
|
38
39
|
cursorFinished,
|
|
@@ -54,8 +55,9 @@ import {
|
|
|
54
55
|
type ShipCaps,
|
|
55
56
|
type StepReturn,
|
|
56
57
|
type UnitEnding,
|
|
57
|
-
type UnitPipelineInput,
|
|
58
58
|
type UnitPipelineState,
|
|
59
|
+
stepPrefixOf,
|
|
60
|
+
type UnitSession,
|
|
59
61
|
} from "../ship/coordinator.js";
|
|
60
62
|
import { checksSettledEventType, isCoordinatorUnit, runFinishedEventType, type CoordinatorUnit } from "./contract.js";
|
|
61
63
|
|
|
@@ -165,12 +167,14 @@ interface PlanFacts {
|
|
|
165
167
|
/** The severity to address, beside `merge`: the level an approve's findings are held to, and which layer set it. */
|
|
166
168
|
addressSeverity: AddressSeverity;
|
|
167
169
|
addressSeveritySource: AddressSeveritySource;
|
|
170
|
+
/** The grant beside them (decision 0046): what a renewal could spend, and which layer granted it. */
|
|
171
|
+
grant: Grant;
|
|
172
|
+
grantSource: GrantSource;
|
|
168
173
|
/** The instance's mark as the plan route answers it: a generated one-unit plan (a `plan` with no `path`). */
|
|
169
174
|
generated: boolean;
|
|
170
175
|
repo: string;
|
|
171
176
|
base: string;
|
|
172
177
|
caps: ShipCaps;
|
|
173
|
-
childMinutes: UnitPipelineInput["childMinutes"];
|
|
174
178
|
units: CoordinatorUnit[];
|
|
175
179
|
}
|
|
176
180
|
|
|
@@ -178,18 +182,27 @@ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "obj
|
|
|
178
182
|
const isMinutes = (v: unknown): v is Record<string, number> =>
|
|
179
183
|
isRecord(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n));
|
|
180
184
|
|
|
185
|
+
/** The grant as the plan route answers it; anything unreadable — a count
|
|
186
|
+
* outside the module's ceiling included — is the default: nothing renews on a guess. */
|
|
187
|
+
function readGrant(raw: unknown): Grant {
|
|
188
|
+
if (
|
|
189
|
+
!isRecord(raw) ||
|
|
190
|
+
typeof raw.renewals !== "number" ||
|
|
191
|
+
!Number.isInteger(raw.renewals) ||
|
|
192
|
+
raw.renewals < 0 ||
|
|
193
|
+
raw.renewals > GRANT_RENEWALS_MAX
|
|
194
|
+
)
|
|
195
|
+
return DEFAULT_GRANT;
|
|
196
|
+
const cap = raw.costCapUsd;
|
|
197
|
+
return { renewals: raw.renewals, ...(typeof cap === "number" && cap > 0 ? { costCapUsd: cap } : {}) };
|
|
198
|
+
}
|
|
199
|
+
|
|
181
200
|
function readPlan(a: BotAnswer): PlanFacts {
|
|
182
201
|
const b = a.body;
|
|
183
202
|
if (b.ok !== true) throw new UnreadableAnswer("plan", a, "not ok");
|
|
184
203
|
if (typeof b.repo !== "string" || typeof b.base !== "string") throw new UnreadableAnswer("plan", a, "repo and base");
|
|
185
204
|
if (!isMinutes(b.caps) || typeof b.caps.maxRounds !== "number" || typeof b.caps.maxMinutes !== "number")
|
|
186
205
|
throw new UnreadableAnswer("plan", a, "caps");
|
|
187
|
-
if (
|
|
188
|
-
!isMinutes(b.childMinutes) ||
|
|
189
|
-
typeof b.childMinutes.coding !== "number" ||
|
|
190
|
-
typeof b.childMinutes.review !== "number"
|
|
191
|
-
)
|
|
192
|
-
throw new UnreadableAnswer("plan", a, "childMinutes");
|
|
193
206
|
const units: unknown = b.units;
|
|
194
207
|
if (!Array.isArray(units) || !units.every(isCoordinatorUnit)) throw new UnreadableAnswer("plan", a, "units");
|
|
195
208
|
return {
|
|
@@ -200,11 +213,13 @@ function readPlan(a: BotAnswer): PlanFacts {
|
|
|
200
213
|
b.addressSeveritySource === "run" || b.addressSeveritySource === "user" || b.addressSeveritySource === "channel"
|
|
201
214
|
? b.addressSeveritySource
|
|
202
215
|
: "org",
|
|
216
|
+
grant: readGrant(b.grant),
|
|
217
|
+
grantSource:
|
|
218
|
+
b.grantSource === "run" || b.grantSource === "user" || b.grantSource === "channel" ? b.grantSource : "org",
|
|
203
219
|
generated: b.generated === true,
|
|
204
220
|
repo: b.repo,
|
|
205
221
|
base: b.base,
|
|
206
222
|
caps: { maxRounds: b.caps.maxRounds, maxMinutes: b.caps.maxMinutes },
|
|
207
|
-
childMinutes: { coding: b.childMinutes.coding, review: b.childMinutes.review },
|
|
208
223
|
units,
|
|
209
224
|
};
|
|
210
225
|
}
|
|
@@ -261,6 +276,10 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
261
276
|
reviewHead,
|
|
262
277
|
dispositions,
|
|
263
278
|
handoff,
|
|
279
|
+
pushed,
|
|
280
|
+
leaseStartedAt,
|
|
281
|
+
costUsd,
|
|
282
|
+
handoffLists,
|
|
264
283
|
} = facts;
|
|
265
284
|
return {
|
|
266
285
|
type: "read-record",
|
|
@@ -278,6 +297,11 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
278
297
|
...(reviewHead !== undefined ? { reviewHead } : {}),
|
|
279
298
|
...(dispositions !== undefined ? { dispositions } : {}),
|
|
280
299
|
...(handoff !== undefined ? { handoff } : {}),
|
|
300
|
+
// The renewal's facts (decision 0046), as the record carries them.
|
|
301
|
+
...(Array.isArray(pushed) ? { pushed } : {}),
|
|
302
|
+
...(typeof leaseStartedAt === "number" ? { leaseStartedAt } : {}),
|
|
303
|
+
...(typeof costUsd === "number" || costUsd === null ? { costUsd } : {}),
|
|
304
|
+
...(handoffLists !== undefined ? { handoffLists } : {}),
|
|
281
305
|
},
|
|
282
306
|
at: a.body.at,
|
|
283
307
|
};
|
|
@@ -466,11 +490,16 @@ async function runUnit(
|
|
|
466
490
|
instanceId: string,
|
|
467
491
|
node: PlanUnitNode,
|
|
468
492
|
plan: PlanFacts,
|
|
493
|
+
session?: UnitSession,
|
|
469
494
|
): Promise<UnitEnding> {
|
|
470
495
|
const unit = node.id;
|
|
496
|
+
// A renewal's segment names its steps under the segment (`U10/s2/…`), so
|
|
497
|
+
// the Workflow's durable step cache never answers segment two with segment
|
|
498
|
+
// one's results (decision 0046).
|
|
499
|
+
const prefix = stepPrefixOf(unit, session);
|
|
471
500
|
const tag = { parentInstanceId: instanceId, unit };
|
|
472
501
|
const start = readUnitStart(
|
|
473
|
-
answerOf("unit-start", await step.do(`${
|
|
502
|
+
answerOf("unit-start", await step.do(`${prefix}/start`, STEP_CONFIG, () => call(bot, "unit-start", tag))),
|
|
474
503
|
);
|
|
475
504
|
// A resume at review (agent-ship item 10) rides the unit's row: the pull
|
|
476
505
|
// request of ship's own the requester named opens the pipeline at its first
|
|
@@ -486,7 +515,6 @@ async function runUnit(
|
|
|
486
515
|
repo: plan.repo,
|
|
487
516
|
base: plan.base,
|
|
488
517
|
caps: plan.caps,
|
|
489
|
-
childMinutes: plan.childMinutes,
|
|
490
518
|
// The instance's field decides who merges (record 0031's merge grant),
|
|
491
519
|
// carried here by the plan route: the hand-off wrote `runner` on a
|
|
492
520
|
// seeded plan and `person` on a task, and the door re-checks it — the
|
|
@@ -494,9 +522,12 @@ async function runUnit(
|
|
|
494
522
|
merge: plan.merge,
|
|
495
523
|
addressSeverity: plan.addressSeverity,
|
|
496
524
|
addressSeveritySource: plan.addressSeveritySource,
|
|
525
|
+
grant: plan.grant,
|
|
526
|
+
grantSource: plan.grantSource,
|
|
497
527
|
generated: plan.generated,
|
|
498
528
|
...(resume !== undefined ? { resume } : {}),
|
|
499
529
|
...(lastPush !== undefined ? { lastPush } : {}),
|
|
530
|
+
...(session !== undefined ? { session } : {}),
|
|
500
531
|
},
|
|
501
532
|
start.at,
|
|
502
533
|
);
|
|
@@ -509,7 +540,7 @@ async function runUnit(
|
|
|
509
540
|
for (const note of transition.notes) {
|
|
510
541
|
if (note.type === "round") {
|
|
511
542
|
const body = { ...tag, index: note.index, agent: note.agent, outcome: note.outcome };
|
|
512
|
-
await step.do(`${
|
|
543
|
+
await step.do(`${prefix}/note/${++notes}`, STEP_CONFIG, () => call(bot, "round", body));
|
|
513
544
|
} else {
|
|
514
545
|
// A merge_ready ending names the pull request as it is at the APPROVED
|
|
515
546
|
// head (agent-ship item 9): one more pr-check reads the facts fresh —
|
|
@@ -521,10 +552,10 @@ async function runUnit(
|
|
|
521
552
|
if (note.ending.kind === "merge_ready") {
|
|
522
553
|
try {
|
|
523
554
|
const check = prCheckReturn(
|
|
524
|
-
`${
|
|
555
|
+
`${prefix}/end/pr-facts`,
|
|
525
556
|
answerOf(
|
|
526
557
|
"pr-check",
|
|
527
|
-
await step.do(`${
|
|
558
|
+
await step.do(`${prefix}/end/pr-facts`, STEP_CONFIG, () => call(bot, "pr-check", tag)),
|
|
528
559
|
),
|
|
529
560
|
);
|
|
530
561
|
if (check.type === "pr-check" && check.pr.state === "merged")
|
|
@@ -548,8 +579,20 @@ async function runUnit(
|
|
|
548
579
|
? { headSha: note.ending.headSha }
|
|
549
580
|
: {}),
|
|
550
581
|
...(state.lastCodingRunId !== undefined ? { codingRunId: state.lastCodingRunId } : {}),
|
|
582
|
+
// A continued ending is a segment's end, not the unit's: the bot
|
|
583
|
+
// writes the renewal as a row keyed by the next segment's index
|
|
584
|
+
// (decision 0046), so a runner reclaimed here never renews twice.
|
|
585
|
+
...(note.ending.kind === "continued"
|
|
586
|
+
? {
|
|
587
|
+
segment: {
|
|
588
|
+
index: note.ending.segment,
|
|
589
|
+
...(note.ending.from !== undefined ? { from: note.ending.from } : {}),
|
|
590
|
+
runId: note.ending.runId,
|
|
591
|
+
},
|
|
592
|
+
}
|
|
593
|
+
: {}),
|
|
551
594
|
};
|
|
552
|
-
await step.do(`${
|
|
595
|
+
await step.do(`${prefix}/end`, STEP_CONFIG, () => call(bot, "unit-end", body));
|
|
553
596
|
}
|
|
554
597
|
}
|
|
555
598
|
}
|
|
@@ -583,13 +626,23 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
583
626
|
const [next] = readyUnits(graph, cursor);
|
|
584
627
|
if (next === undefined) break;
|
|
585
628
|
cursor = startUnit(graph, cursor, next);
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
)
|
|
629
|
+
const node = graph.units.find((u) => u.id === next)!;
|
|
630
|
+
let ending = await runUnit(step, bot, instanceId, node, plan);
|
|
631
|
+
// The lease continues while the grant renews (decision 0046): each
|
|
632
|
+
// `continued` ending opens the next segment of the same unit — a fresh
|
|
633
|
+
// pipeline under a fresh lease, from the recorded sha, briefed with the
|
|
634
|
+
// previous segment's write-up — until the unit ends some other way.
|
|
635
|
+
while (ending.kind === "continued") {
|
|
636
|
+
const c = ending;
|
|
637
|
+
ending = await runUnit(step, bot, instanceId, node, plan, {
|
|
638
|
+
segment: c.segment,
|
|
639
|
+
renewalsSpent: c.segment - 1,
|
|
640
|
+
spendUsd: c.spendUsd,
|
|
641
|
+
...(c.from !== undefined ? { continueFrom: c.from } : {}),
|
|
642
|
+
previousRunId: c.runId,
|
|
643
|
+
...(c.handoff !== undefined ? { previousHandoff: c.handoff } : {}),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
593
646
|
endings[next] = ending.kind;
|
|
594
647
|
cursor = settleUnit(graph, cursor, next, ending.kind === "merged" ? "done" : "failed");
|
|
595
648
|
}
|