@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
|
@@ -209,6 +209,14 @@ export interface RunRecord {
|
|
|
209
209
|
* (docs/reference/specs/resident-repos.md item 29) — read off the record,
|
|
210
210
|
* never off the reply's text. */
|
|
211
211
|
pr?: RunPullRequest;
|
|
212
|
+
/** The run's lease as the harness started it (item 2; decision 0046): the
|
|
213
|
+
* `lease` event, folded at the assembly. Present only on a run whose
|
|
214
|
+
* harness started a loop; a record written before the event lacks it. */
|
|
215
|
+
lease?: RunLease;
|
|
216
|
+
/** The heads the run pushed (item 2; decision 0046): one per branch, the
|
|
217
|
+
* last `pushed_head` event's sha winning — the fact a renewal reads to know
|
|
218
|
+
* the run made progress. Present only on a run that pushed. */
|
|
219
|
+
pushed?: PushedHead[];
|
|
212
220
|
/** What the run cost in tokens, per model, summed from its `model.turn`
|
|
213
221
|
* spans at finish (`usageOfEvents`; docs/reference/specs/costs.md, cost by user).
|
|
214
222
|
* Every record written since carries it (zero turns included); one written
|
|
@@ -225,6 +233,39 @@ export interface RunPullRequest {
|
|
|
225
233
|
head?: string;
|
|
226
234
|
}
|
|
227
235
|
|
|
236
|
+
/** A run's lease as the record names it (item 2): when the wall clock
|
|
237
|
+
* began, when the lease ends, and where the loop was cut so the write-up
|
|
238
|
+
* and the post-step run inside it. */
|
|
239
|
+
export interface RunLease {
|
|
240
|
+
startedAt: number;
|
|
241
|
+
endsAt: number;
|
|
242
|
+
loopEndsAt: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** A head a run pushed, as the record names it (item 2): the branch and the sha. */
|
|
246
|
+
export interface PushedHead {
|
|
247
|
+
ref: string;
|
|
248
|
+
sha: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The heads a run's events say it pushed — one per branch, the last event's
|
|
252
|
+
* sha winning, in first-seen order — or nothing when it pushed none. */
|
|
253
|
+
export function pushedHeadsOf(events: readonly RunEvent[]): PushedHead[] | undefined {
|
|
254
|
+
const byRef = new Map<string, string>();
|
|
255
|
+
for (const e of events) if (e.type === "pushed_head") byRef.set(e.ref, e.sha);
|
|
256
|
+
if (byRef.size === 0) return undefined;
|
|
257
|
+
return [...byRef].map(([ref, sha]) => ({ ref, sha }));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The lease a run's events say its harness started — the first `lease`
|
|
261
|
+
* event; a resumed run's later generations publish none — or nothing. */
|
|
262
|
+
export function leaseOfEvents(events: readonly RunEvent[]): RunLease | undefined {
|
|
263
|
+
for (const e of events) {
|
|
264
|
+
if (e.type === "lease") return { startedAt: e.startedAt, endsAt: e.endsAt, loopEndsAt: e.loopEndsAt };
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
228
269
|
/** The pull request a run's events say it opened or edited — the last
|
|
229
270
|
* `pr_opened` wins, as an edit after an open names the same PR — or nothing. */
|
|
230
271
|
export function prOfEvents(events: readonly RunEvent[]): RunPullRequest | undefined {
|
|
@@ -465,6 +506,10 @@ export interface RunListOptions {
|
|
|
465
506
|
/** The runs one run spawned or that continue a thread it opened
|
|
466
507
|
* (`RunRecord.parentRunId`, item 46) — a conductor's children as one listing. */
|
|
467
508
|
parentRunId?: string;
|
|
509
|
+
/** The runs that name one pull request (`namesPullRequest`, item 58): the
|
|
510
|
+
* coding runs whose post-step opened or edited it and the review runs that
|
|
511
|
+
* posted to it — the findings ledger's runs as one listing. */
|
|
512
|
+
pr?: { repo: string; number: number };
|
|
468
513
|
/** What the ACTOR may see (authorization): the store predicate compiled
|
|
469
514
|
* from the policy, pushed down so no surface loads rows and filters after.
|
|
470
515
|
* Absent = no visibility constraint — only a caller that has already decided
|
|
@@ -577,6 +622,27 @@ export function matchesVisibility(
|
|
|
577
622
|
}
|
|
578
623
|
}
|
|
579
624
|
|
|
625
|
+
/** The pull request number a stored row names (item 58): the one its coding
|
|
626
|
+
* post-step opened or edited (`pr`), else the one its review posted to
|
|
627
|
+
* (`reviewPost.target`). Undefined for a run that named none — a review whose
|
|
628
|
+
* post was skipped included. The Worker's `pr_number` column is this value,
|
|
629
|
+
* written at `put` and backfilled from `summary_json` with the same rule. */
|
|
630
|
+
export function pullRequestNumberOf(row: Pick<RunListItem, "pr" | "reviewPost">): number | undefined {
|
|
631
|
+
if (row.pr !== undefined) return row.pr.number;
|
|
632
|
+
if (row.reviewPost?.posted) return row.reviewPost.target.number;
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** The one truth table for `RunListOptions.pr`: the row's repository is the
|
|
637
|
+
* pull request's and the number it names is the pull request's. The Worker
|
|
638
|
+
* applies the same test in SQL (`repo = ? AND pr_number = ?`). */
|
|
639
|
+
export function namesPullRequest(
|
|
640
|
+
row: Pick<RunListItem, "repo" | "pr" | "reviewPost">,
|
|
641
|
+
pr: { repo: string; number: number },
|
|
642
|
+
): boolean {
|
|
643
|
+
return row.repo === pr.repo && pullRequestNumberOf(row) === pr.number;
|
|
644
|
+
}
|
|
645
|
+
|
|
580
646
|
/** The list order (`finishedAt` desc, `id` desc) as a cursor predicate: true
|
|
581
647
|
* when `row` comes strictly after the cursor. Shared by `selectListItems`;
|
|
582
648
|
* the Worker applies the same predicate in SQL. */
|
|
@@ -751,6 +817,18 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
751
817
|
if (r.profile !== undefined && !isRunProfileRecord(r.profile)) return false;
|
|
752
818
|
// The pull request the run reached (item 2): a positive integer and a URL, or absent.
|
|
753
819
|
if (r.pr !== undefined && !isRunPullRequestShape(r.pr)) return false;
|
|
820
|
+
if (
|
|
821
|
+
r.pushed !== undefined &&
|
|
822
|
+
!(
|
|
823
|
+
Array.isArray(r.pushed) &&
|
|
824
|
+
r.pushed.every((h) => {
|
|
825
|
+
if (typeof h !== "object" || h === null) return false;
|
|
826
|
+
const { ref, sha } = h as Record<string, unknown>;
|
|
827
|
+
return typeof ref === "string" && ref.length > 0 && typeof sha === "string" && /^[0-9a-f]{7,40}$/.test(sha);
|
|
828
|
+
})
|
|
829
|
+
)
|
|
830
|
+
)
|
|
831
|
+
return false;
|
|
754
832
|
// A parent is named by a run id (item 46): the same shape as the record's own.
|
|
755
833
|
if (r.parentRunId !== undefined && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
|
|
756
834
|
return false;
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import type { RunEvent } from "./runEvents.js";
|
|
2
2
|
|
|
3
|
-
// What a run cost in tokens, and who it belongs to — the data behind
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
3
|
+
// What a run cost in tokens, and who it belongs to — the data behind the cost
|
|
4
|
+
// dimensions of the costs page (docs/reference/specs/costs.md items 10–10a) and
|
|
5
|
+
// the dollars on a run's own page. Every provider call a run makes is one
|
|
6
|
+
// `model.turn` span with the provider's own token counts as attrs (metered by
|
|
7
|
+
// the model proxy or the runner; docs/reference/specs/tracing.md), so a run's
|
|
8
|
+
// usage is the sum of those spans, per model. It is computed ONCE, at finish,
|
|
9
|
+
// from the events still in memory (`assembleRunRecord`), and rides the record —
|
|
10
|
+
// a record's events may be cut to fit the byte budget, so an aggregate taken
|
|
11
|
+
// then is more faithful than one re-read later. A record written before the
|
|
12
|
+
// field existed has none; the store fills it in from the run's stored events
|
|
13
|
+
// on demand (the lazy backfill), and reports how many still wait.
|
|
14
|
+
//
|
|
15
|
+
// The store answers one row per run (`RunUsageRows`); the bot folds them into
|
|
16
|
+
// cells (`aggregateUsage`) — one per requester, thread, channel, agent and UTC
|
|
17
|
+
// day of finish — so a snapshot holds the cube every dimension is read from
|
|
18
|
+
// and never a row per run.
|
|
13
19
|
|
|
14
20
|
export interface ModelUsage {
|
|
15
21
|
turns: number;
|
|
@@ -28,6 +34,9 @@ export interface RunUsage {
|
|
|
28
34
|
|
|
29
35
|
export const UNKNOWN_MODEL = "unknown";
|
|
30
36
|
|
|
37
|
+
/** The agent of a cell whose record names none (a record written before the field, a hand-built one). */
|
|
38
|
+
export const UNKNOWN_AGENT = "unknown";
|
|
39
|
+
|
|
31
40
|
const MODEL_TURN = "model.turn";
|
|
32
41
|
|
|
33
42
|
const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0);
|
|
@@ -96,7 +105,7 @@ export function isRunUsage(v: unknown): v is RunUsage {
|
|
|
96
105
|
return Object.values(u.byModel).every(isModelUsage);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
|
-
// ---- the
|
|
108
|
+
// ---- the store's answer: one row per run -------------------------------------------------
|
|
100
109
|
|
|
101
110
|
/** One finished run as the aggregate sees it — the record's identity fields and its usage. */
|
|
102
111
|
export interface UsageRun {
|
|
@@ -105,22 +114,19 @@ export interface UsageRun {
|
|
|
105
114
|
userName?: string;
|
|
106
115
|
/** A child run is billed to whoever started its parent (run-history item 46). */
|
|
107
116
|
parentRunId?: string;
|
|
117
|
+
/** The thread the run ran in and the channel it belongs to (platform-namespaced, invariant 4). */
|
|
118
|
+
threadKey: string;
|
|
119
|
+
channelId: string;
|
|
120
|
+
/** The agent the run resolved to; absent on a record that names none. */
|
|
121
|
+
agent?: string;
|
|
108
122
|
startedAt: number;
|
|
109
123
|
finishedAt: number;
|
|
110
124
|
/** Absent on a record written before usage existed and not yet backfilled. */
|
|
111
125
|
usage?: RunUsage;
|
|
112
126
|
}
|
|
113
127
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
userName?: string;
|
|
117
|
-
/** The UTC day the run finished, `YYYY-MM-DD`. */
|
|
118
|
-
day: string;
|
|
119
|
-
runs: number;
|
|
120
|
-
/** Summed wall-clock of the runs (finish − start), for allocating shared cloud spend. */
|
|
121
|
-
wallMs: number;
|
|
122
|
-
usage: RunUsage;
|
|
123
|
-
}
|
|
128
|
+
/** Whose run it is, as far as billing goes. */
|
|
129
|
+
export type UsageIdentity = Pick<UsageRun, "userId" | "userName">;
|
|
124
130
|
|
|
125
131
|
export interface RunUsageQuery {
|
|
126
132
|
/** Runs that finished at or after this epoch ms … */
|
|
@@ -129,8 +135,43 @@ export interface RunUsageQuery {
|
|
|
129
135
|
untilMs: number;
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
/** What the store answers (`POST /runs/usage`): the runs that finished in the
|
|
139
|
+
* range, oldest finish first, and the identity of every parent a child names
|
|
140
|
+
* that is outside the batch and known to the store — so the bot can bill the
|
|
141
|
+
* child without a second read. */
|
|
142
|
+
export interface RunUsageRows {
|
|
143
|
+
runs: UsageRun[];
|
|
144
|
+
parents: Record<string, UsageIdentity>;
|
|
145
|
+
/** Runs in range whose usage is not known yet (written before the field; backfill outstanding). */
|
|
146
|
+
pending: number;
|
|
147
|
+
/** The oldest finish the store still holds, so a page can bound its range to the data. */
|
|
148
|
+
earliestFinishedAt?: number;
|
|
149
|
+
retentionDays: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- the aggregate: the usage cube --------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
/** One cell: the runs one requester was billed for in one thread, one channel,
|
|
155
|
+
* on one agent, finishing on one UTC day. Every cost dimension is a sum over
|
|
156
|
+
* these — by user, thread, channel or agent along a key, by model inside `usage`. */
|
|
157
|
+
export interface UsageRow {
|
|
158
|
+
/** The UTC day the runs finished, `YYYY-MM-DD`. */
|
|
159
|
+
day: string;
|
|
160
|
+
/** The requester billed (a child's parent's). */
|
|
161
|
+
userId: string;
|
|
162
|
+
userName?: string;
|
|
163
|
+
threadKey: string;
|
|
164
|
+
channelId: string;
|
|
165
|
+
/** The agent, or `unknown` when the record names none. */
|
|
166
|
+
agent: string;
|
|
167
|
+
runs: number;
|
|
168
|
+
/** Summed wall-clock of the runs (finish − start), for allocating shared cloud spend. */
|
|
169
|
+
wallMs: number;
|
|
170
|
+
usage: RunUsage;
|
|
171
|
+
}
|
|
172
|
+
|
|
132
173
|
export interface RunUsageReport {
|
|
133
|
-
rows:
|
|
174
|
+
rows: UsageRow[];
|
|
134
175
|
/** Runs in range whose usage is not known yet (written before the field; backfill outstanding). */
|
|
135
176
|
pending: number;
|
|
136
177
|
/** The oldest finish the store still holds, so a page can bound its range to the data. */
|
|
@@ -140,34 +181,51 @@ export interface RunUsageReport {
|
|
|
140
181
|
|
|
141
182
|
export const dayOf = (epochMs: number): string => new Date(epochMs).toISOString().slice(0, 10);
|
|
142
183
|
|
|
184
|
+
const identityOf = (who: UsageIdentity): UsageIdentity => ({
|
|
185
|
+
userId: who.userId,
|
|
186
|
+
...(who.userName ? { userName: who.userName } : {}),
|
|
187
|
+
});
|
|
188
|
+
|
|
143
189
|
/** Who a run is billed to: its parent's requester when it is a child and the
|
|
144
190
|
* parent is known (in the batch, or through `lookupParent`), else its own. */
|
|
145
191
|
export function billedTo(
|
|
146
192
|
run: UsageRun,
|
|
147
193
|
batch: ReadonlyMap<string, UsageRun>,
|
|
148
|
-
lookupParent: (id: string) =>
|
|
149
|
-
):
|
|
150
|
-
if (!run.parentRunId) return
|
|
194
|
+
lookupParent: (id: string) => UsageIdentity | undefined,
|
|
195
|
+
): UsageIdentity {
|
|
196
|
+
if (!run.parentRunId) return identityOf(run);
|
|
151
197
|
const parent = batch.get(run.parentRunId) ?? lookupParent(run.parentRunId);
|
|
152
|
-
|
|
153
|
-
return { userId: parent.userId, ...(parent.userName ? { userName: parent.userName } : {}) };
|
|
198
|
+
return identityOf(parent ?? run);
|
|
154
199
|
}
|
|
155
200
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
201
|
+
const CELL_ORDER = ["day", "userId", "threadKey", "channelId", "agent"] as const;
|
|
202
|
+
|
|
203
|
+
/** Pure: the runs summed into cells — per (billed user, thread, channel, agent,
|
|
204
|
+
* UTC day of finish). A run without usage counts as pending and contributes its
|
|
205
|
+
* run and wall-clock only. Rows come out oldest day first, then by user id,
|
|
206
|
+
* thread, channel and agent. */
|
|
207
|
+
export function aggregateUsage(
|
|
160
208
|
runs: readonly UsageRun[],
|
|
161
|
-
lookupParent: (id: string) =>
|
|
162
|
-
): { rows:
|
|
209
|
+
lookupParent: (id: string) => UsageIdentity | undefined = () => undefined,
|
|
210
|
+
): { rows: UsageRow[]; pending: number } {
|
|
163
211
|
const batch = new Map(runs.map((r) => [r.id, r]));
|
|
164
|
-
const rows = new Map<string,
|
|
212
|
+
const rows = new Map<string, UsageRow>();
|
|
165
213
|
let pending = 0;
|
|
166
214
|
for (const run of runs) {
|
|
167
215
|
const who = billedTo(run, batch, lookupParent);
|
|
168
216
|
const day = dayOf(run.finishedAt);
|
|
169
|
-
const
|
|
170
|
-
const
|
|
217
|
+
const agent = run.agent ?? UNKNOWN_AGENT;
|
|
218
|
+
const key = JSON.stringify([day, who.userId, run.threadKey, run.channelId, agent]);
|
|
219
|
+
const row = rows.get(key) ?? {
|
|
220
|
+
day,
|
|
221
|
+
userId: who.userId,
|
|
222
|
+
threadKey: run.threadKey,
|
|
223
|
+
channelId: run.channelId,
|
|
224
|
+
agent,
|
|
225
|
+
runs: 0,
|
|
226
|
+
wallMs: 0,
|
|
227
|
+
usage: emptyUsage(),
|
|
228
|
+
};
|
|
171
229
|
if (who.userName && !row.userName) row.userName = who.userName;
|
|
172
230
|
row.runs += 1;
|
|
173
231
|
row.wallMs += Math.max(0, run.finishedAt - run.startedAt);
|
|
@@ -175,25 +233,78 @@ export function aggregateUsageByUser(
|
|
|
175
233
|
else pending += 1;
|
|
176
234
|
rows.set(key, row);
|
|
177
235
|
}
|
|
236
|
+
const sorted = [...rows.values()].sort((a, b) => {
|
|
237
|
+
for (const k of CELL_ORDER) {
|
|
238
|
+
if (a[k] < b[k]) return -1;
|
|
239
|
+
if (a[k] > b[k]) return 1;
|
|
240
|
+
}
|
|
241
|
+
return 0;
|
|
242
|
+
});
|
|
243
|
+
return { rows: sorted, pending };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The bot's fold over the store's answer: the cells, the parents outside the batch consulted. */
|
|
247
|
+
export function reportOfUsageRows(rows: RunUsageRows): RunUsageReport {
|
|
248
|
+
const { rows: cells, pending } = aggregateUsage(rows.runs, (id) => rows.parents[id]);
|
|
178
249
|
return {
|
|
179
|
-
rows:
|
|
180
|
-
|
|
250
|
+
rows: cells,
|
|
251
|
+
// The store counts what it could not price; the fold sees the same runs without `usage`.
|
|
252
|
+
pending: Math.max(pending, rows.pending),
|
|
253
|
+
...(rows.earliestFinishedAt !== undefined ? { earliestFinishedAt: rows.earliestFinishedAt } : {}),
|
|
254
|
+
retentionDays: rows.retentionDays,
|
|
181
255
|
};
|
|
182
256
|
}
|
|
183
257
|
|
|
258
|
+
const isIdentity = (v: unknown): v is UsageIdentity =>
|
|
259
|
+
typeof v === "object" &&
|
|
260
|
+
v !== null &&
|
|
261
|
+
typeof (v as UsageIdentity).userId === "string" &&
|
|
262
|
+
((v as UsageIdentity).userName === undefined || typeof (v as UsageIdentity).userName === "string");
|
|
263
|
+
|
|
264
|
+
function isUsageRun(v: unknown): v is UsageRun {
|
|
265
|
+
if (!isIdentity(v)) return false;
|
|
266
|
+
const r = v as UsageRun;
|
|
267
|
+
return (
|
|
268
|
+
typeof r.id === "string" &&
|
|
269
|
+
typeof r.threadKey === "string" &&
|
|
270
|
+
typeof r.channelId === "string" &&
|
|
271
|
+
(r.agent === undefined || typeof r.agent === "string") &&
|
|
272
|
+
(r.parentRunId === undefined || typeof r.parentRunId === "string") &&
|
|
273
|
+
typeof r.startedAt === "number" &&
|
|
274
|
+
typeof r.finishedAt === "number" &&
|
|
275
|
+
(r.usage === undefined || isRunUsage(r.usage))
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function hasReportTail(r: Record<string, unknown>): boolean {
|
|
280
|
+
if (typeof r.pending !== "number" || typeof r.retentionDays !== "number") return false;
|
|
281
|
+
return r.earliestFinishedAt === undefined || typeof r.earliestFinishedAt === "number";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The store's wire shape, as the Worker client re-validates it. */
|
|
285
|
+
export function isRunUsageRows(v: unknown): v is RunUsageRows {
|
|
286
|
+
if (typeof v !== "object" || v === null) return false;
|
|
287
|
+
const r = v as Record<string, unknown>;
|
|
288
|
+
if (!Array.isArray(r.runs) || !r.runs.every(isUsageRun)) return false;
|
|
289
|
+
if (typeof r.parents !== "object" || r.parents === null || Array.isArray(r.parents)) return false;
|
|
290
|
+
if (!Object.values(r.parents).every(isIdentity)) return false;
|
|
291
|
+
return hasReportTail(r);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The cells' wire shape, as the snapshot guard re-validates it. */
|
|
184
295
|
export function isRunUsageReport(v: unknown): v is RunUsageReport {
|
|
185
296
|
if (typeof v !== "object" || v === null) return false;
|
|
186
297
|
const r = v as Record<string, unknown>;
|
|
187
|
-
if (!Array.isArray(r.rows) ||
|
|
188
|
-
if (r.earliestFinishedAt !== undefined && typeof r.earliestFinishedAt !== "number") return false;
|
|
298
|
+
if (!Array.isArray(r.rows) || !hasReportTail(r)) return false;
|
|
189
299
|
return r.rows.every(
|
|
190
300
|
(row) =>
|
|
191
|
-
|
|
192
|
-
row
|
|
193
|
-
typeof (row as
|
|
194
|
-
typeof (row as
|
|
195
|
-
typeof (row as
|
|
196
|
-
typeof (row as
|
|
197
|
-
|
|
301
|
+
isIdentity(row) &&
|
|
302
|
+
typeof (row as UsageRow).day === "string" &&
|
|
303
|
+
typeof (row as UsageRow).threadKey === "string" &&
|
|
304
|
+
typeof (row as UsageRow).channelId === "string" &&
|
|
305
|
+
typeof (row as UsageRow).agent === "string" &&
|
|
306
|
+
typeof (row as UsageRow).runs === "number" &&
|
|
307
|
+
typeof (row as UsageRow).wallMs === "number" &&
|
|
308
|
+
isRunUsage((row as UsageRow).usage),
|
|
198
309
|
);
|
|
199
310
|
}
|
|
@@ -7,6 +7,9 @@ import { parseIngressTokenMap, tokenForSubject } from "./ingressTokens.js";
|
|
|
7
7
|
// worker), and each Worker's `scheduled()` looks its firing up HERE — so a
|
|
8
8
|
// schedule can never exist in one place and not the other.
|
|
9
9
|
//
|
|
10
|
+
// Schedules a person adds at runtime, and the hand-over of a firing to the
|
|
11
|
+
// `schedule:<name>` actor declared below, are docs/decisions/0049-a-stored-schedule-is-a-turn-the-minute-tick-fires.md.
|
|
12
|
+
//
|
|
10
13
|
// A schedule has three independent facets:
|
|
11
14
|
// worker — whose wrangler.jsonc carries the cron and whose `scheduled()` fires it
|
|
12
15
|
// action — what a firing does: `run` (the bot shim POSTs the generic /ingress as
|
|
@@ -431,7 +431,8 @@ function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
|
|
|
431
431
|
`Push the branch as soon as the change exists and its cheapest proving checks pass — before the project's ` +
|
|
432
432
|
`full verification, which runs after that push with any fix as a further commit; an unpushed tree does not ` +
|
|
433
433
|
`survive the run's end. Right before each push, fetch ${onto} again and rebase once more if it moved while ` +
|
|
434
|
-
`you worked, so the pull request is not born conflicting
|
|
434
|
+
`you worked, so the pull request is not born conflicting. At the wind-down note, commit and push what compiles, ` +
|
|
435
|
+
`say what does not, then answer.`
|
|
435
436
|
);
|
|
436
437
|
}
|
|
437
438
|
|