@coreplane/switchboard 1.241.0 → 1.243.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 (157) hide show
  1. package/dist/assets/config/config.example.yaml +45 -13
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +86 -29
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +298 -24
  4. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +38 -5
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +1 -1
  7. package/dist/assets/project.json +5 -5
  8. package/dist/assets/source.json +3 -3
  9. package/dist/assets/src/agents/registry.ts +61 -48
  10. package/dist/assets/src/config/profile.ts +68 -3
  11. package/dist/assets/src/core/authz/actor.ts +12 -9
  12. package/dist/assets/src/core/authz/authorize.ts +17 -4
  13. package/dist/assets/src/core/authz/policy.ts +4 -0
  14. package/dist/assets/src/core/authz/types.ts +12 -0
  15. package/dist/assets/src/core/budgets.ts +313 -0
  16. package/dist/assets/src/core/coordinator/contract.ts +3 -1
  17. package/dist/assets/src/core/coordinator/driver.ts +0 -10
  18. package/dist/assets/src/core/costs.ts +21 -80
  19. package/dist/assets/src/core/harness/scope.ts +21 -0
  20. package/dist/assets/src/core/modelPricing.ts +212 -0
  21. package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
  22. package/dist/assets/src/core/reviewVerdict.ts +7 -0
  23. package/dist/assets/src/core/runEvents.ts +48 -5
  24. package/dist/assets/src/core/runFriction.ts +2 -1
  25. package/dist/assets/src/core/runLedger/types.ts +2 -0
  26. package/dist/assets/src/core/runRecord.ts +51 -0
  27. package/dist/assets/src/core/runUsage.ts +158 -47
  28. package/dist/assets/src/core/schedules.ts +3 -0
  29. package/dist/assets/src/core/ship/contract.ts +4 -2
  30. package/dist/assets/src/core/ship/coordinator.ts +101 -84
  31. package/dist/assets/src/core/ship/handoff.ts +9 -0
  32. package/dist/assets/src/execution/bashTimeout.ts +8 -5
  33. package/dist/assets/src/execution/residentAutoRebuild.ts +4 -2
  34. package/dist/assets/src/execution/residentHead.ts +23 -10
  35. package/dist/assets/src/execution/residentInfraStreak.ts +89 -0
  36. package/dist/assets/src/execution/residentRefresh.ts +43 -4
  37. package/dist/assets/src/execution/residentSteps.ts +1 -0
  38. package/dist/assets/src/execution/sandboxErrors.ts +6 -0
  39. package/dist/assets/src/execution/seedPlan.ts +17 -0
  40. package/dist/assets/web/dist/.vite/manifest.json +443 -446
  41. package/dist/assets/web/dist/assets/{AppShell-DEy5jRuS.js → AppShell-IDMGG6yi.js} +1 -1
  42. package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +2 -0
  43. package/dist/assets/web/dist/assets/{DeliveryPage-BGpPr5xr.js → DeliveryPage-n1tz5I_v.js} +1 -1
  44. package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +2 -0
  45. package/dist/assets/web/dist/assets/{NotFoundPage-DtmQM2gD.js → NotFoundPage-C97EhJcQ.js} +1 -1
  46. package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +1 -0
  47. package/dist/assets/web/dist/assets/{ResidentDetailPage-C-48fxat.js → ResidentDetailPage-DACalNVF.js} +1 -1
  48. package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +1 -0
  49. package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +1 -0
  50. package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +9 -0
  51. package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +1 -0
  52. package/dist/assets/web/dist/assets/{RunsTabs-CtNGNnhN.js → RunsTabs-BUfdk0lH.js} +1 -1
  53. package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +1 -0
  54. package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +1 -0
  55. package/dist/assets/web/dist/assets/{StatusDot-DPWE1JmO.js → StatusDot-Dw0T1M-P.js} +1 -1
  56. package/dist/assets/web/dist/assets/{Tooltip-DKhSRH9t.js → Tooltip-BbLuIAiS.js} +1 -1
  57. package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +1 -0
  58. package/dist/assets/web/dist/assets/{angular-html-DgSK1qvr.js → angular-html-oBNfPJR0.js} +1 -1
  59. package/dist/assets/web/dist/assets/{angular-ts-D3gNdiSG.js → angular-ts-BvNwsyWA.js} +1 -1
  60. package/dist/assets/web/dist/assets/{apl-CkHCYM8I.js → apl-CNUdRlYf.js} +1 -1
  61. package/dist/assets/web/dist/assets/{astro-DLm45axt.js → astro-Zb0NriSe.js} +1 -1
  62. package/dist/assets/web/dist/assets/{blade-BKa-VE-c.js → blade-qPRVheqq.js} +1 -1
  63. package/dist/assets/web/dist/assets/{c-C8NCNWai.js → c-D8Awx4YO.js} +1 -1
  64. package/dist/assets/web/dist/assets/{chapel-DqDQ7IKH.js → chapel-Bt72Mhsx.js} +1 -1
  65. package/dist/assets/web/dist/assets/{cobol-j4vCD6AJ.js → cobol-BOBacexg.js} +1 -1
  66. package/dist/assets/web/dist/assets/{coffee-C6oDVH-0.js → coffee-E4u0liHW.js} +1 -1
  67. package/dist/assets/web/dist/assets/{cpp-Bd3A1Rtc.js → cpp-q2sLNlul.js} +1 -1
  68. package/dist/assets/web/dist/assets/{crystal-CUOgOw0d.js → crystal-DyWqUnlb.js} +1 -1
  69. package/dist/assets/web/dist/assets/{css-RYljyv7G.js → css-CQY0hFsD.js} +1 -1
  70. package/dist/assets/web/dist/assets/{dist-BnJXKHCY.js → dist-twkFmSUY.js} +2 -2
  71. package/dist/assets/web/dist/assets/durationTone-BobbycC-.js +1 -0
  72. package/dist/assets/web/dist/assets/{edge-BJhNdQDc.js → edge-C1MwhJkX.js} +1 -1
  73. package/dist/assets/web/dist/assets/{elixir-g1AWOAfT.js → elixir-Bb3YbHfn.js} +1 -1
  74. package/dist/assets/web/dist/assets/{elm-CER5e4Dz.js → elm-DAN9IGQw.js} +1 -1
  75. package/dist/assets/web/dist/assets/{erb-D5aOmMQf.js → erb-BEB8Xlsj.js} +1 -1
  76. package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
  77. package/dist/assets/web/dist/assets/{git-rebase-D6rfV8jp.js → git-rebase-tqpjRfxO.js} +1 -1
  78. package/dist/assets/web/dist/assets/{glimmer-js-BvDH3-mC.js → glimmer-js-Ccbo65zR.js} +1 -1
  79. package/dist/assets/web/dist/assets/{glimmer-ts-DChtNs9V.js → glimmer-ts-B6WBVMpE.js} +1 -1
  80. package/dist/assets/web/dist/assets/{glsl-BHlsWrxf.js → glsl-tRec3Fcu.js} +1 -1
  81. package/dist/assets/web/dist/assets/{graphql-BsaBHb53.js → graphql-P8kbxT4F.js} +1 -1
  82. package/dist/assets/web/dist/assets/{hack-0lbDosCd.js → hack-H9Zhkagy.js} +1 -1
  83. package/dist/assets/web/dist/assets/{haml-BKtco2vr.js → haml-DtEnpn7Z.js} +1 -1
  84. package/dist/assets/web/dist/assets/{handlebars-BE_cj04z.js → handlebars-DkgPfoAz.js} +1 -1
  85. package/dist/assets/web/dist/assets/{html-BNcr8EVd.js → html-D30RXpIs.js} +1 -1
  86. package/dist/assets/web/dist/assets/{html-derivative-BsJt2Kei.js → html-derivative-DwozLrEx.js} +1 -1
  87. package/dist/assets/web/dist/assets/{http-BR5P8ER_.js → http-DXuzBAPm.js} +1 -1
  88. package/dist/assets/web/dist/assets/{hurl-CtHalpWS.js → hurl-d1UUJIt_.js} +1 -1
  89. package/dist/assets/web/dist/assets/indexRow-B_s5tKyq.js +1 -0
  90. package/dist/assets/web/dist/assets/{java-BXVdhN11.js → java-DL0gWf34.js} +1 -1
  91. package/dist/assets/web/dist/assets/{javascript-CyoShgNQ.js → javascript-Cl7vavnS.js} +1 -1
  92. package/dist/assets/web/dist/assets/{jinja-BQYZzHex.js → jinja-CfQOWMX9.js} +1 -1
  93. package/dist/assets/web/dist/assets/{jison-DqrUa1Eq.js → jison-KdYirlqm.js} +1 -1
  94. package/dist/assets/web/dist/assets/{json-CQiTF9Jj.js → json-C9cDQ-Qj.js} +1 -1
  95. package/dist/assets/web/dist/assets/{jsx-Y16MkCYQ.js → jsx-CRx5NItd.js} +1 -1
  96. package/dist/assets/web/dist/assets/{julia-zn16YONe.js → julia-CmsQsZQl.js} +1 -1
  97. package/dist/assets/web/dist/assets/{just-DcVeN_MN.js → just-CsM3Q8TE.js} +1 -1
  98. package/dist/assets/web/dist/assets/{latex-BNNoF-WP.js → latex-BXCh5YRX.js} +1 -1
  99. package/dist/assets/web/dist/assets/{liquid-DAi6qHzn.js → liquid-BF2vwK8p.js} +1 -1
  100. package/dist/assets/web/dist/assets/{lua-BswS0axw.js → lua-DcMBATrl.js} +1 -1
  101. package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +1 -0
  102. package/dist/assets/web/dist/assets/{main-B3is4sk6.js → main-d-w-tIKt.js} +2 -2
  103. package/dist/assets/web/dist/assets/{marko-C-4hkmQZ.js → marko-88MndKvG.js} +1 -1
  104. package/dist/assets/web/dist/assets/{mdc-0ZaHUkS7.js → mdc-DJ4kVAd8.js} +1 -1
  105. package/dist/assets/web/dist/assets/{nginx-DBDg5tOR.js → nginx-CP6mRgtV.js} +1 -1
  106. package/dist/assets/web/dist/assets/{nim-BVMz49Yw.js → nim-QQfj3fpF.js} +1 -1
  107. package/dist/assets/web/dist/assets/{org-1hO_K6ya.js → org-Bke3eBzc.js} +1 -1
  108. package/dist/assets/web/dist/assets/{perl-CVRQLvVk.js → perl-Bkh0N7Kz.js} +1 -1
  109. package/dist/assets/web/dist/assets/{php-K9nCrB9n.js → php-Dnv1Piya.js} +1 -1
  110. package/dist/assets/web/dist/assets/{pug-CTxOcmO3.js → pug-D6peFFI7.js} +1 -1
  111. package/dist/assets/web/dist/assets/{qml-CmCkfG5q.js → qml-D8PEs-C-.js} +1 -1
  112. package/dist/assets/web/dist/assets/{r-pP47Xn1X.js → r-B1EL9b_j.js} +1 -1
  113. package/dist/assets/web/dist/assets/{razor-BpW6r3tC.js → razor-BsMTIh2b.js} +1 -1
  114. package/dist/assets/web/dist/assets/{regexp-CINgdY4N.js → regexp-HhvC8spD.js} +1 -1
  115. package/dist/assets/web/dist/assets/{rst-BZIUEq2Q.js → rst-D5paAxpg.js} +1 -1
  116. package/dist/assets/web/dist/assets/{ruby-nzAOxz6r.js → ruby-BrQwhLrl.js} +1 -1
  117. package/dist/assets/web/dist/assets/{sas-QIS1bFth.js → sas-afot2B1o.js} +1 -1
  118. package/dist/assets/web/dist/assets/{scss-BuhBBVNt.js → scss-Efm-mwuG.js} +1 -1
  119. package/dist/assets/web/dist/assets/{shellscript-CQc1vXbk.js → shellscript-BK0Vv5fT.js} +1 -1
  120. package/dist/assets/web/dist/assets/{shellsession-CoubCAUv.js → shellsession-BGqlMC7N.js} +1 -1
  121. package/dist/assets/web/dist/assets/{soy-CPRzlder.js → soy-M0b4UwGM.js} +1 -1
  122. package/dist/assets/web/dist/assets/{sql-W9krb8-9.js → sql-sxe6IE9j.js} +1 -1
  123. package/dist/assets/web/dist/assets/sseReplay-C9m_EB8J.js +9 -0
  124. package/dist/assets/web/dist/assets/{stata-CuISJEC0.js → stata-nPF_ddLP.js} +1 -1
  125. package/dist/assets/web/dist/assets/{surrealql-CSet7584.js → surrealql-D_GrC6u7.js} +1 -1
  126. package/dist/assets/web/dist/assets/{svelte-ChXTSYwK.js → svelte-DwL1AtNP.js} +1 -1
  127. package/dist/assets/web/dist/assets/{templ-Bm55v62k.js → templ-7s7LTDkc.js} +1 -1
  128. package/dist/assets/web/dist/assets/{tex-CICMX8Gj.js → tex-Bx-5fMxe.js} +1 -1
  129. package/dist/assets/web/dist/assets/{ts-tags-CA1UzWyB.js → ts-tags-DUMJnke_.js} +1 -1
  130. package/dist/assets/web/dist/assets/{tsx-Dy04HNbv.js → tsx-BN8biPDe.js} +1 -1
  131. package/dist/assets/web/dist/assets/{twig-BBHsVnVD.js → twig-8nIu84TN.js} +1 -1
  132. package/dist/assets/web/dist/assets/{typescript-Dvc-wVBT.js → typescript-BooSPq_S.js} +1 -1
  133. package/dist/assets/web/dist/assets/{typst-Dvlqx3_q.js → typst-CTBiBsem.js} +1 -1
  134. package/dist/assets/web/dist/assets/{vue-zWi1MNU2.js → vue-C6Ft4Lea.js} +1 -1
  135. package/dist/assets/web/dist/assets/{vue-html-D4YxT3An.js → vue-html-Ba36dD5D.js} +1 -1
  136. package/dist/assets/web/dist/assets/{vue-vine-0YE7uQlH.js → vue-vine-NkFexVo2.js} +1 -1
  137. package/dist/assets/web/dist/assets/{xml-gm-iksZB.js → xml-C_THnHXZ.js} +1 -1
  138. package/dist/assets/web/dist/assets/{xsl-D_W5BcrY.js → xsl-D8G5xqjY.js} +1 -1
  139. package/dist/assets/web/dist/assets/{yaml-CHmZ21wZ.js → yaml-jAMIJzge.js} +1 -1
  140. package/dist/cli.js +4121 -1902
  141. package/package.json +1 -1
  142. package/dist/assets/web/dist/assets/CostsPage-BuHD-Bv7.js +0 -2
  143. package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +0 -2
  144. package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +0 -1
  145. package/dist/assets/web/dist/assets/ResidentsIndexPage-CBBIjPOa.js +0 -1
  146. package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +0 -1
  147. package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +0 -6
  148. package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +0 -1
  149. package/dist/assets/web/dist/assets/ScheduledPage-Ci1Ygqnc.js +0 -1
  150. package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +0 -1
  151. package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +0 -1
  152. package/dist/assets/web/dist/assets/UnitRoutePage-DEN5bSQf.js +0 -1
  153. package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +0 -1
  154. package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
  155. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
  156. package/dist/assets/web/dist/assets/main-DP_zSemY.css +0 -1
  157. package/dist/assets/web/dist/assets/sseReplay-yji8a1wM.js +0 -9
@@ -11,7 +11,11 @@ providers:
11
11
  anthropic:
12
12
  type: anthropic
13
13
  apiKeyEnv: ANTHROPIC_API_KEY
14
- # Any OpenAI-compatible endpoint works with the same adapter:
14
+ # OpenAI itself runs on the same compatible adapter: any preset's model may
15
+ # be openai/<model-id> anywhere a model is accepted. The adapter speaks Chat
16
+ # Completions, not the Responses API, sends no reasoning control (our effort
17
+ # tiers do not reach these models), and meters tokens without pricing them.
18
+ # A first-class OpenAI block is a plan's business, not this file's.
15
19
  openai:
16
20
  type: openai-compatible
17
21
  baseUrl: https://api.openai.com/v1
@@ -65,10 +69,19 @@ defaults:
65
69
  # clipped (the card and the run record say so); a preset whose identity or
66
70
  # class is above a cap is refused by name before any executor exists. Absent →
67
71
  # nothing is capped and every preset runs exactly its declared profile.
72
+ # A fourth field, `confirm`, is the front door's and caps no run: the first
73
+ # blast-radius class a command the router bound from prose is handed back at
74
+ # (`To run this: …`) instead of run — `write` (every write is handed back; a
75
+ # read or a repository's own test or build runs) or `destructive` (a write
76
+ # runs at once; only a destructive one is handed back). It intersects toward
77
+ # caution like the run axes — a channel or a user can only ask MORE than what
78
+ # is set here — and unset everywhere it is the built-in `write`. `exec` and
79
+ # `never` are refused by name.
68
80
  # boundary:
69
81
  # maxMinutes: 120
70
82
  # maxIdentity: write
71
83
  # machines: [none, blank, repo-cold, repo-resident]
84
+ # confirm: write
72
85
 
73
86
  # PR reviews and the reading diff (docs/reference/specs/reading-diff.md). Every
74
87
  # review records its full `git diff`; the ABRIDGED version (meat.dev, run on
@@ -100,10 +113,16 @@ defaults:
100
113
  # maxMinutes: 45
101
114
  # maxIdentity: read
102
115
 
103
- # Static per-user defaults (user ID -> scope).
116
+ # Static per-user defaults (user ID -> scope). A user's `harness` puts that
117
+ # person's runs of a preset on another harness (docs/reference/specs/harness.md
118
+ # item 8) — every run of it they request, in any channel, and nobody else's; the
119
+ # same words as the top-level `harness` block, which they override (a channel's
120
+ # `harness` sits between). From chat: `config set me --harness.coding opencode`.
104
121
  # users:
105
122
  # slack:U012345:
106
123
  # model: openai/gpt-5
124
+ # harness:
125
+ # coding: opencode
107
126
 
108
127
  # Authorization (docs/reference/specs/authorization.md item 9, docs/reference/authorization.md):
109
128
  # two blocks. `restrict` names what is CLOSED unless granted — an agent listed
@@ -327,17 +346,22 @@ workspaceDir: ./workspaces
327
346
  # # a finding at or above it sends the round into a fix step instead of merge-ready.
328
347
  # # Per channel/user: config set … --ship.addressSeverity; per run: severity:<level>.
329
348
 
330
- # Which harness each preset's runs are driven by (docs/reference/specs/harness.md
331
- # item 8): a mapping of preset to a harness's name. Two exist — `pi` (the pi
332
- # coding agent, the default for every preset) and `opencode` (OpenCode's server,
333
- # `opencode serve`) — each started inside the run's own execution container (or
334
- # as a child of the bot for a preset without a workspace) with the run's
335
- # model-proxy bearer as its only key, every tool call decided in the bot, every
336
- # event on the run's record. A preset the block does not name runs on pi;
337
- # nothing defaults to OpenCode. A run keeps the harness it started on, so
338
- # changing a word moves the next run, never one in flight. Any other word
339
- # (`native`, the deleted loop; `codex`) fails the load naming the two, as does
340
- # an unknown preset. How-to: docs/how-to/put-a-preset-on-opencode.md.
349
+ # Which harness each preset's runs are driven by, deployment-wide
350
+ # (docs/reference/specs/harness.md item 8): a mapping of preset to a harness's
351
+ # name. Two exist — `pi` (the pi coding agent, the default for every preset)
352
+ # and `opencode` (OpenCode's server, `opencode serve`) — each started inside
353
+ # the run's own execution container (or as a child of the bot for a preset
354
+ # without a workspace) with the run's model-proxy bearer as its only key, every
355
+ # tool call decided in the bot, every event on the run's record. This block is
356
+ # the defaults layer of the per-scope `harness` setting: a channel's or a
357
+ # user's `harness` (above, or `config set channel|me --harness.<preset> …`)
358
+ # overrides it for the runs in that scope, user over channel over here. A
359
+ # preset no layer names runs on pi; nothing defaults to OpenCode. A run keeps
360
+ # the harness it started on, so changing a word moves the next run, never one
361
+ # in flight. Any other word (`native`, the deleted loop; `codex`) fails the
362
+ # load naming the two, as does an unknown preset, in every layer; a
363
+ # `defaults.harness` key is refused pointing here. How-to:
364
+ # docs/how-to/put-a-preset-on-opencode.md.
341
365
  # harness:
342
366
  # coding: opencode # coding runs on OpenCode
343
367
  # review: pi # the default, written out
@@ -411,6 +435,8 @@ workspaceDir: ./workspaces
411
435
  # snapshot: # the page serves a snapshot of both billing sources, never a live read;
412
436
  # everyHours: 24 # how often the bot reads them (1–168, default 24). `costs snapshot`
413
437
  # # takes one now. Kept on the state Worker any `*.worker` block names
438
+ # alertChannel: slack:C0123ABCD # optional: a line there after three failed takes in a row, and one
439
+ # # when a take lands again; otherwise only the page's status line says
414
440
  # groups:
415
441
  # myapp:
416
442
  # label: My App
@@ -423,6 +449,12 @@ workspaceDir: ./workspaces
423
449
  # r2Buckets: # optional: bucket name → label for buckets not named <worker>-cache
424
450
  # some-other-bucket: uploads
425
451
  # anthropicWorkspaceId: wrkspc_... # this app's Anthropic workspace
452
+ # prices: # optional: USD per million tokens by <provider>/<model>, the ref a run's
453
+ # openai/gpt-5: # spans name — for a model the Anthropic list does not know, or to
454
+ # input: 1.25 # override the list; all four kinds named (a provider with no
455
+ # output: 10 # cache-write charge says 0). A model neither knows is "unpriced",
456
+ # cacheRead: 0.125 # never $0, on the costs page and on a run's own page
457
+ # cacheWrite: 0
426
458
 
427
459
  # Delivery indicators: GET /delivery and `delivery report` (docs/reference/specs/delivery.md).
428
460
  # Optional. Needs a GitHub credential (the App triple or GH_TOKEN); read-only on GitHub.
@@ -25,6 +25,7 @@ import {
25
25
  isRunSession,
26
26
  isRunVisibilityFilter,
27
27
  normalizeStored,
28
+ pullRequestNumberOf,
28
29
  RETENTION_BOUNDS,
29
30
  RUN_EVENTS_DEFAULT_PAGE,
30
31
  RUN_EVENTS_MAX_PAGE,
@@ -59,11 +60,11 @@ import {
59
60
  } from "../../src/core/runLedger/sessionLog.ts";
60
61
  import type { RunEvent } from "../../src/core/runEvents.ts";
61
62
  import {
62
- aggregateUsageByUser,
63
63
  isRunUsage,
64
64
  usageOfEvents,
65
65
  type RunUsage,
66
- type RunUsageReport,
66
+ type RunUsageRows,
67
+ type UsageIdentity,
67
68
  type UsageRun,
68
69
  } from "../../src/core/runUsage.ts";
69
70
  import {
@@ -1265,13 +1266,15 @@ export class RunHistoryDO extends DurableObject<Env> {
1265
1266
  this.migrateRunsTable();
1266
1267
  // The indexes the visibility predicate's leaves walk (`channel_id IN`,
1267
1268
  // `channel_visibility IN`, `user_id =`), each ordered like the page; the
1268
- // session the sweep asks about; the parent a children listing filters on.
1269
+ // session the sweep asks about; the parent a children listing filters on;
1270
+ // the pull request a findings listing filters on.
1269
1271
  this.sql.exec(`
1270
1272
  CREATE INDEX IF NOT EXISTS runs_channel_finished ON runs(channel_id, finished_at DESC, run_id DESC);
1271
1273
  CREATE INDEX IF NOT EXISTS runs_visibility_finished ON runs(channel_visibility, finished_at DESC, run_id DESC);
1272
1274
  CREATE INDEX IF NOT EXISTS runs_user_finished ON runs(user_id, finished_at DESC, run_id DESC);
1273
1275
  CREATE INDEX IF NOT EXISTS runs_session ON runs(session_key);
1274
1276
  CREATE INDEX IF NOT EXISTS runs_parent ON runs(parent_run_id, finished_at DESC, run_id DESC);
1277
+ CREATE INDEX IF NOT EXISTS runs_pr ON runs(repo, pr_number, finished_at DESC, run_id DESC);
1275
1278
  `);
1276
1279
  // The sessions registry (session-log item 7): every session log a run of
1277
1280
  // this store claimed, with its thread — the sweep cannot enumerate the
@@ -1438,7 +1441,11 @@ export class RunHistoryDO extends DurableObject<Env> {
1438
1441
  * aggregate fills it from the run's stored events. The parent a child names
1439
1442
  * (run-history item 46), the column a children listing filters on: the
1440
1443
  * record already carries it in `summary_json`, so existing rows are filled
1441
- * from there once, and every later `put` writes it beside the row. */
1444
+ * from there once, and every later `put` writes it beside the row. The pull
1445
+ * request a run names (run-history item 58; `pullRequestNumberOf`), the
1446
+ * column a findings listing filters on: the same one-time fill from
1447
+ * `summary_json` — the coding post-step's `pr`, else a posted review's
1448
+ * target — and every later `put` writes it beside the row. */
1442
1449
  migrateRunsTable(): void {
1443
1450
  const columns = new Set(
1444
1451
  this.sql
@@ -1457,6 +1464,19 @@ export class RunHistoryDO extends DurableObject<Env> {
1457
1464
  WHERE json_type(summary_json, '$.parentRunId') = 'text'`,
1458
1465
  );
1459
1466
  }
1467
+ if (!columns.has("pr_number")) {
1468
+ this.sql.exec(`ALTER TABLE runs ADD COLUMN pr_number INTEGER`);
1469
+ // `pullRequestNumberOf` in SQL: the coding post-step's pull request, else
1470
+ // the one a posted review targeted (a skipped post has no target).
1471
+ this.sql.exec(
1472
+ `UPDATE runs SET pr_number = COALESCE(
1473
+ CASE WHEN json_type(summary_json, '$.pr.number') = 'integer' THEN json_extract(summary_json, '$.pr.number') END,
1474
+ CASE WHEN json_extract(summary_json, '$.reviewPost.posted') = 1
1475
+ AND json_type(summary_json, '$.reviewPost.target.number') = 'integer'
1476
+ THEN json_extract(summary_json, '$.reviewPost.target.number') END
1477
+ )`,
1478
+ );
1479
+ }
1460
1480
  }
1461
1481
 
1462
1482
  private liveByThread(threadKey: string): LiveRunRow | undefined {
@@ -1970,8 +1990,8 @@ export class RunHistoryDO extends DurableObject<Env> {
1970
1990
  );
1971
1991
  this.sql.exec(
1972
1992
  `INSERT INTO runs (run_id, label, agent, model, channel_id, user_id, thread_key, channel_visibility, repo, started_at, finished_at, stored_at, status,
1973
- event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json, parent_run_id)
1974
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1993
+ event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json, parent_run_id, pr_number)
1994
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1975
1995
  ON CONFLICT(run_id) DO UPDATE SET
1976
1996
  label = excluded.label, agent = excluded.agent, model = excluded.model, channel_id = excluded.channel_id,
1977
1997
  user_id = excluded.user_id, thread_key = excluded.thread_key, channel_visibility = excluded.channel_visibility,
@@ -1981,7 +2001,8 @@ export class RunHistoryDO extends DurableObject<Env> {
1981
2001
  bytes = excluded.bytes, diagnosis_json = excluded.diagnosis_json, summary_json = excluded.summary_json,
1982
2002
  session_key = excluded.session_key,
1983
2003
  usage_json = COALESCE(excluded.usage_json, runs.usage_json),
1984
- parent_run_id = excluded.parent_run_id`,
2004
+ parent_run_id = excluded.parent_run_id,
2005
+ pr_number = excluded.pr_number`,
1985
2006
  stored.id,
1986
2007
  stored.label ?? null,
1987
2008
  stored.agent ?? null,
@@ -2004,6 +2025,7 @@ export class RunHistoryDO extends DurableObject<Env> {
2004
2025
  stored.session?.key ?? null,
2005
2026
  stored.usage ? JSON.stringify(stored.usage) : null,
2006
2027
  stored.parentRunId ?? null,
2028
+ pullRequestNumberOf(stored) ?? null,
2007
2029
  );
2008
2030
  // The session's registry row learns its newest finish (session-log item
2009
2031
  // 7); a record that reaches the store without a claim (the plain put
@@ -2208,28 +2230,32 @@ export class RunHistoryDO extends DurableObject<Env> {
2208
2230
  }
2209
2231
 
2210
2232
  /**
2211
- * Who spent what, per UTC day (costs.md, cost by user), over the runs that
2212
- * finished in [sinceMs, untilMs) and are inside the retention window. A row
2213
- * written before `usage_json` existed is filled in here from its stored
2214
- * `model.turn` events — up to USAGE_BACKFILL_PER_CALL a call, the rest
2215
- * reported as `pending` — so the history heals as it is read, with no
2233
+ * What the runs that finished in [sinceMs, untilMs) and are inside the
2234
+ * retention window cost (costs.md items 10–10a; run-history item 56): one row
2235
+ * per run — its requester, thread, channel, agent and usage — plus the
2236
+ * identity of every parent a child names that is outside the batch, so the
2237
+ * bot bills the child without a second read. The arithmetic is the bot's; the
2238
+ * Worker only reads its rows. A row written before `usage_json` existed is
2239
+ * filled in here from its stored `model.turn` events — up to
2240
+ * USAGE_BACKFILL_PER_CALL a call, the rest answered without `usage` and
2241
+ * counted in `pending` — so the history heals as it is read, with no
2216
2242
  * operator step; a run without turns is written back as the zero usage so it
2217
- * is not re-read. A child is billed to its parent's requester; a parent
2218
- * outside the range is looked up by id.
2243
+ * is not re-read.
2219
2244
  */
2220
- usageByUser(sinceMs: number, untilMs: number): RunUsageReport {
2245
+ usage(sinceMs: number, untilMs: number): RunUsageRows {
2221
2246
  const now = systemClock();
2222
2247
  const { policy } = this.policyState();
2223
2248
  const cutoff = now - policy.retentionDays * 86_400_000;
2224
2249
  const rows = this.sql
2225
2250
  .exec<UsageRow>(
2226
- `SELECT run_id, user_id, started_at, finished_at, usage_json, summary_json FROM runs
2251
+ `SELECT run_id, user_id, agent, channel_id, thread_key, started_at, finished_at, usage_json, summary_json FROM runs
2227
2252
  WHERE finished_at >= ? AND finished_at < ? ORDER BY finished_at ASC, run_id ASC`,
2228
2253
  Math.max(sinceMs, cutoff),
2229
2254
  untilMs,
2230
2255
  )
2231
2256
  .toArray();
2232
2257
  let backfilled = 0;
2258
+ let pending = 0;
2233
2259
  const runs: UsageRun[] = rows.map((row) => {
2234
2260
  let usage = parseUsageJson(row.usage_json);
2235
2261
  if (usage === undefined && backfilled < USAGE_BACKFILL_PER_CALL) {
@@ -2237,31 +2263,38 @@ export class RunHistoryDO extends DurableObject<Env> {
2237
2263
  this.sql.exec(`UPDATE runs SET usage_json = ? WHERE run_id = ?`, JSON.stringify(usage), row.run_id);
2238
2264
  backfilled += 1;
2239
2265
  }
2266
+ if (usage === undefined) pending += 1;
2240
2267
  const who = identityOfSummary(row.summary_json);
2241
2268
  return {
2242
2269
  id: row.run_id,
2243
2270
  userId: row.user_id,
2244
2271
  ...(who.userName ? { userName: who.userName } : {}),
2245
2272
  ...(who.parentRunId ? { parentRunId: who.parentRunId } : {}),
2273
+ threadKey: row.thread_key,
2274
+ channelId: row.channel_id,
2275
+ ...(row.agent ? { agent: row.agent } : {}),
2246
2276
  startedAt: row.started_at,
2247
2277
  finishedAt: row.finished_at,
2248
2278
  ...(usage ? { usage } : {}),
2249
2279
  };
2250
2280
  });
2251
- const lookupParent = (id: string) => {
2281
+ // The parents a child names that are not in the batch: looked up once each, known or not.
2282
+ const inBatch = new Set(runs.map((r) => r.id));
2283
+ const parents: Record<string, UsageIdentity> = {};
2284
+ for (const id of new Set(runs.map((r) => r.parentRunId).filter((p): p is string => !!p && !inBatch.has(p)))) {
2252
2285
  const p = this.sql
2253
2286
  .exec<{ user_id: string; summary_json: string }>(`SELECT user_id, summary_json FROM runs WHERE run_id = ?`, id)
2254
2287
  .toArray()[0];
2255
- if (!p) return undefined;
2288
+ if (!p) continue;
2256
2289
  const who = identityOfSummary(p.summary_json);
2257
- return { userId: p.user_id, ...(who.userName ? { userName: who.userName } : {}) };
2258
- };
2259
- const { rows: out, pending } = aggregateUsageByUser(runs, lookupParent);
2290
+ parents[id] = { userId: p.user_id, ...(who.userName ? { userName: who.userName } : {}) };
2291
+ }
2260
2292
  const earliest = this.sql
2261
2293
  .exec<{ m: number | null }>(`SELECT MIN(finished_at) AS m FROM runs WHERE finished_at >= ?`, cutoff)
2262
2294
  .one().m;
2263
2295
  return {
2264
- rows: out,
2296
+ runs,
2297
+ parents,
2265
2298
  pending,
2266
2299
  ...(earliest !== null ? { earliestFinishedAt: earliest } : {}),
2267
2300
  retentionDays: policy.retentionDays,
@@ -2343,6 +2376,11 @@ export class RunHistoryDO extends DurableObject<Env> {
2343
2376
  where.push(`parent_run_id = ?`);
2344
2377
  params.push(q.parentRunId);
2345
2378
  }
2379
+ if (q.pr !== undefined) {
2380
+ // `namesPullRequest` in SQL: the row's repository and the number it names.
2381
+ where.push(`repo = ?`, `pr_number = ?`);
2382
+ params.push(q.pr.repo, q.pr.number);
2383
+ }
2346
2384
  if (q.visibleTo !== undefined && q.visibleTo.kind !== "all") where.push(visibilitySql(q.visibleTo, params));
2347
2385
  const select = `SELECT run_id, agent, channel_id, finished_at, bytes, event_count, summary_json FROM runs WHERE ${where.join(" AND ")} ORDER BY finished_at DESC, run_id DESC`;
2348
2386
  // `LIMIT` holds on the over-bound path too: the kept set is the newest
@@ -2423,6 +2461,9 @@ const USAGE_BACKFILL_PER_CALL = 200;
2423
2461
  type UsageRow = {
2424
2462
  run_id: string;
2425
2463
  user_id: string;
2464
+ agent: string | null;
2465
+ channel_id: string;
2466
+ thread_key: string;
2426
2467
  started_at: number;
2427
2468
  finished_at: number;
2428
2469
  usage_json: string | null;
@@ -2590,12 +2631,26 @@ function parseRunList(body: unknown): Validated<{ storeKey: string; query: RunLi
2590
2631
  if (!id.ok) return invalid("parentRunId must match ^[A-Za-z0-9_-]{1,64}$");
2591
2632
  query.parentRunId = id.value;
2592
2633
  }
2634
+ if (b.pr !== undefined) {
2635
+ const pr = b.pr as Record<string, unknown> | null;
2636
+ if (
2637
+ typeof pr !== "object" ||
2638
+ pr === null ||
2639
+ typeof pr.repo !== "string" ||
2640
+ !REPO_SLUG.test(pr.repo) ||
2641
+ typeof pr.number !== "number" ||
2642
+ !Number.isInteger(pr.number) ||
2643
+ pr.number < 1
2644
+ )
2645
+ return invalid("pr must be { repo: owner/name, number: a positive integer }");
2646
+ query.pr = { repo: pr.repo, number: pr.number };
2647
+ }
2593
2648
  if (b.visibleTo !== undefined) {
2594
2649
  // A malformed filter is a 400, never "all": the bot degrades to live rows
2595
2650
  // rather than the DO widening what an actor may see.
2596
2651
  if (!isRunVisibilityFilter(b.visibleTo)) return invalid("visibleTo must be a run visibility filter");
2597
- if (boundParameters(b.visibleTo) > DO_MAX_BOUND_PARAMETERS - RUN_LIST_BASE_PARAMETERS)
2598
- return invalid(`visibleTo names more than ${DO_MAX_BOUND_PARAMETERS - RUN_LIST_BASE_PARAMETERS} ids`);
2652
+ const headroom = DO_MAX_BOUND_PARAMETERS - RUN_LIST_BASE_PARAMETERS - (query.pr ? RUN_LIST_PR_PARAMETERS : 0);
2653
+ if (boundParameters(b.visibleTo) > headroom) return invalid(`visibleTo names more than ${headroom} ids`);
2599
2654
  query.visibleTo = b.visibleTo;
2600
2655
  }
2601
2656
  return { ok: true, value: { storeKey: key.value, query } };
@@ -2604,6 +2659,8 @@ function parseRunList(body: unknown): Validated<{ storeKey: string; query: RunLi
2604
2659
  /** Parameters the page query binds before any filter: the cursor pair (3) and the age floor (1),
2605
2660
  * plus `agent`, `channel`, `threadKey`, `parentRunId`, and the LIMIT at most — the headroom `visibleTo` must fit under. */
2606
2661
  const RUN_LIST_BASE_PARAMETERS = 9;
2662
+ /** The two more a `pr` filter binds (`repo`, `pr_number`), taken from the same headroom only when asked. */
2663
+ const RUN_LIST_PR_PARAMETERS = 2;
2607
2664
 
2608
2665
  /** How many `?` a filter binds (one per id, one per user). */
2609
2666
  function boundParameters(f: RunVisibilityFilter): number {
@@ -3800,13 +3857,13 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
3800
3857
  );
3801
3858
  return json(result ?? { events: null });
3802
3859
  }
3803
- if (pathname === "/runs/usage-by-user") {
3860
+ if (pathname === "/runs/usage") {
3804
3861
  const parsed = parseRunUsageQuery(body);
3805
3862
  if (!parsed.ok) return json({ error: parsed.error }, 400);
3806
3863
  const { storeKey, sinceMs, untilMs } = parsed.value;
3807
- const report = await stub(storeKey).usageByUser(sinceMs, untilMs);
3808
- console.log(`[runs/usage-by-user] ${storeKey} -> ${report.rows.length} rows, ${report.pending} pending`);
3809
- return json(report);
3864
+ const rows = await stub(storeKey).usage(sinceMs, untilMs);
3865
+ console.log(`[runs/usage] ${storeKey} -> ${rows.runs.length} runs, ${rows.pending} pending`);
3866
+ return json(rows);
3810
3867
  }
3811
3868
  // /runs/delete
3812
3869
  const parsed = parseRunTarget(body);
@@ -3831,7 +3888,7 @@ const ROUTES = new Set([
3831
3888
  "/runs/summary",
3832
3889
  "/runs/list",
3833
3890
  "/runs/events",
3834
- "/runs/usage-by-user",
3891
+ "/runs/usage",
3835
3892
  "/runs/delete",
3836
3893
  ...LEDGER_ROUTES,
3837
3894
  ]);