@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.
Files changed (153) hide show
  1. package/dist/assets/config/config.example.yaml +20 -1
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +234 -29
  3. package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
  4. package/dist/assets/deploy/cloudflare-resident/worker.ts +483 -75
  5. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
  6. package/dist/assets/package-lock.json +3 -3
  7. package/dist/assets/package.json +1 -1
  8. package/dist/assets/project.json +3 -3
  9. package/dist/assets/source.json +3 -3
  10. package/dist/assets/src/agents/registry.ts +31 -35
  11. package/dist/assets/src/config/profile.ts +68 -3
  12. package/dist/assets/src/core/authz/policy.ts +4 -0
  13. package/dist/assets/src/core/authz/types.ts +10 -0
  14. package/dist/assets/src/core/budgets.ts +349 -0
  15. package/dist/assets/src/core/coordinator/contract.ts +32 -0
  16. package/dist/assets/src/core/coordinator/driver.ts +75 -22
  17. package/dist/assets/src/core/costs.ts +9 -76
  18. package/dist/assets/src/core/delivery.ts +1 -1
  19. package/dist/assets/src/core/modelPricing.ts +212 -0
  20. package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
  21. package/dist/assets/src/core/reviewVerdict.ts +7 -0
  22. package/dist/assets/src/core/runEvents.ts +72 -5
  23. package/dist/assets/src/core/runFriction.ts +3 -1
  24. package/dist/assets/src/core/runRecord.ts +78 -0
  25. package/dist/assets/src/core/runUsage.ts +158 -47
  26. package/dist/assets/src/core/schedules.ts +3 -0
  27. package/dist/assets/src/core/ship/contract.ts +2 -1
  28. package/dist/assets/src/core/ship/coordinator.ts +312 -97
  29. package/dist/assets/src/core/ship/handoff.ts +9 -0
  30. package/dist/assets/src/core/ship/renewal.ts +155 -0
  31. package/dist/assets/src/core/trace/attrs.ts +11 -0
  32. package/dist/assets/src/core/trace/types.ts +4 -3
  33. package/dist/assets/src/execution/bashTimeout.ts +8 -5
  34. package/dist/assets/src/execution/residentRefresh.ts +52 -7
  35. package/dist/assets/src/execution/residentSteps.ts +1 -1
  36. package/dist/assets/src/execution/residentText.ts +8 -3
  37. package/dist/assets/src/mcp/registry.ts +3 -0
  38. package/dist/assets/web/dist/.vite/manifest.json +432 -429
  39. package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
  40. package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
  41. package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
  42. package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
  43. package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
  44. package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +1 -0
  45. package/dist/assets/web/dist/assets/{ResidentDetailPage-DTMBgnIW.js → ResidentDetailPage-C5USV_8P.js} +1 -1
  46. package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +1 -0
  47. package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
  48. package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
  49. package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
  50. package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
  51. package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +1 -0
  52. package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
  53. package/dist/assets/web/dist/assets/{StatusDot-ELoXHlFt.js → StatusDot-_Q_bmReh.js} +1 -1
  54. package/dist/assets/web/dist/assets/{Tooltip-BoeFwYP2.js → Tooltip-Cafsgd4m.js} +1 -1
  55. package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +1 -0
  56. package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
  57. package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
  58. package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
  59. package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
  60. package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
  61. package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
  62. package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
  63. package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
  64. package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
  65. package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
  66. package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
  67. package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
  68. package/dist/assets/web/dist/assets/{dist-BU5UivXC.js → dist-BtPs2AkQ.js} +2 -2
  69. package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
  70. package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
  71. package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
  72. package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
  73. package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
  74. package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
  75. package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
  76. package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
  77. package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
  78. package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
  79. package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
  80. package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
  81. package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
  82. package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
  83. package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
  84. package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
  85. package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
  86. package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +1 -0
  87. package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
  88. package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
  89. package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
  90. package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
  91. package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
  92. package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
  93. package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
  94. package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
  95. package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
  96. package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
  97. package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
  98. package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
  99. package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
  100. package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
  101. package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
  102. package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
  103. package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
  104. package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
  105. package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
  106. package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
  107. package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
  108. package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
  109. package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
  110. package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
  111. package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
  112. package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
  113. package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
  114. package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
  115. package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
  116. package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
  117. package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
  118. package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
  119. package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
  120. package/dist/assets/web/dist/assets/{sseReplay-g7ml86LM.js → sseReplay-DaRtc7nI.js} +5 -5
  121. package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
  122. package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
  123. package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
  124. package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
  125. package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
  126. package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
  127. package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
  128. package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
  129. package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
  130. package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
  131. package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
  132. package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
  133. package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
  134. package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
  135. package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
  136. package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
  137. package/dist/cli.js +5843 -2246
  138. package/package.json +1 -1
  139. package/dist/assets/web/dist/assets/CostsPage-CtmKhhOF.js +0 -2
  140. package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
  141. package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +0 -2
  142. package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +0 -1
  143. package/dist/assets/web/dist/assets/ResidentsIndexPage-CaDvXhzJ.js +0 -1
  144. package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +0 -1
  145. package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +0 -6
  146. package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +0 -1
  147. package/dist/assets/web/dist/assets/ScheduledPage-DXD2gLJk.js +0 -1
  148. package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +0 -1
  149. package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +0 -1
  150. package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
  151. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
  152. package/dist/assets/web/dist/assets/main-B2fX10aW.css +0 -1
  153. 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 "cost by
4
- // user" on the costs page (docs/reference/specs/costs.md). Every provider call a
5
- // run makes is one `model.turn` span with the provider's own token counts as
6
- // attrs (metered by the model proxy or the runner; docs/reference/specs/tracing.md), so a
7
- // run's usage is the sum of those spans, per model. It is computed ONCE, at
8
- // finish, from the events still in memory (`assembleRunRecord`), and rides the
9
- // record — a record's events may be cut to fit the byte budget, so an aggregate
10
- // taken then is more faithful than one re-read later. A record written before
11
- // the field existed has none; the store fills it in from the run's stored
12
- // events on demand (the lazy backfill), and reports how many still wait.
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 aggregate: who spent what, per UTC day ---------------------------------------
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
- export interface UserDayUsage {
115
- userId: string;
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: UserDayUsage[];
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) => Pick<UsageRun, "userId" | "userName"> | undefined,
149
- ): Pick<UsageRun, "userId" | "userName"> {
150
- if (!run.parentRunId) return { userId: run.userId, ...(run.userName ? { userName: run.userName } : {}) };
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
- if (!parent) return { userId: run.userId, ...(run.userName ? { userName: run.userName } : {}) };
153
- return { userId: parent.userId, ...(parent.userName ? { userName: parent.userName } : {}) };
198
+ return identityOf(parent ?? run);
154
199
  }
155
200
 
156
- /** Pure: the runs summed per (billed user, UTC day of finish). A run without
157
- * usage counts as pending and contributes its run and wall-clock only. Rows
158
- * come out oldest day first, then by user id. */
159
- export function aggregateUsageByUser(
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) => Pick<UsageRun, "userId" | "userName"> | undefined = () => undefined,
162
- ): { rows: UserDayUsage[]; pending: number } {
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, UserDayUsage>();
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 key = `${day} ${who.userId}`;
170
- const row = rows.get(key) ?? { userId: who.userId, day, runs: 0, wallMs: 0, usage: emptyUsage() };
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: [...rows.values()].sort((a, b) => (a.day < b.day ? -1 : a.day > b.day ? 1 : a.userId < b.userId ? -1 : 1)),
180
- pending,
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) || typeof r.pending !== "number" || typeof r.retentionDays !== "number") return false;
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
- typeof row === "object" &&
192
- row !== null &&
193
- typeof (row as UserDayUsage).userId === "string" &&
194
- typeof (row as UserDayUsage).day === "string" &&
195
- typeof (row as UserDayUsage).runs === "number" &&
196
- typeof (row as UserDayUsage).wallMs === "number" &&
197
- isRunUsage((row as UserDayUsage).usage),
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