@coreplane/switchboard 1.238.0 → 1.240.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 (155) hide show
  1. package/dist/assets/config/config.example.yaml +6 -0
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +115 -1
  3. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +7 -1
  4. package/dist/assets/deploy/cloudflare-resident/worker.ts +223 -65
  5. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +8 -1
  6. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +337 -44
  7. package/dist/assets/deploy/cloudflare-sandbox/wrangler.template.jsonc +19 -0
  8. package/dist/assets/deploy/secrets.manifest.json +4 -4
  9. package/dist/assets/package-lock.json +207 -3
  10. package/dist/assets/package.json +2 -1
  11. package/dist/assets/source.json +3 -3
  12. package/dist/assets/src/agents/registry.ts +82 -0
  13. package/dist/assets/src/core/authz/actor.ts +33 -2
  14. package/dist/assets/src/core/authz/policy.ts +6 -0
  15. package/dist/assets/src/core/coordinator/contract.ts +12 -1
  16. package/dist/assets/src/core/coordinator/driver.ts +13 -0
  17. package/dist/assets/src/core/costs.ts +1114 -0
  18. package/dist/assets/src/core/costsSnapshotStore.ts +209 -0
  19. package/dist/assets/src/core/delivery.ts +4 -11
  20. package/dist/assets/src/core/deliverySnapshotStore.ts +12 -28
  21. package/dist/assets/src/core/ingressTokens.ts +27 -13
  22. package/dist/assets/src/core/runLedger/types.ts +3 -0
  23. package/dist/assets/src/core/runRecord.ts +10 -3
  24. package/dist/assets/src/core/schedules.ts +2 -1
  25. package/dist/assets/src/core/ship/coordinator.ts +70 -0
  26. package/dist/assets/src/core/snapshotAge.ts +30 -0
  27. package/dist/assets/src/core/stateWorkerRef.ts +41 -0
  28. package/dist/assets/src/execution/residentAutoRebuild.ts +73 -0
  29. package/dist/assets/src/execution/residentRefresh.ts +25 -3
  30. package/dist/assets/src/execution/restoreWaiters.ts +61 -0
  31. package/dist/assets/src/execution/seedPlan.ts +267 -0
  32. package/dist/assets/src/mcp/registry.ts +7 -0
  33. package/dist/assets/web/dist/.vite/manifest.json +507 -427
  34. package/dist/assets/web/dist/assets/AppShell-CM_fh-WW.js +1 -0
  35. package/dist/assets/web/dist/assets/CostsPage-BD3w5j_S.js +2 -0
  36. package/dist/assets/web/dist/assets/DeliveryPage-BtvzKA1m.js +1 -0
  37. package/dist/assets/web/dist/assets/HomePage-BU6aRCSe.js +2 -0
  38. package/dist/assets/web/dist/assets/NotFoundPage-DwOX1HaZ.js +1 -0
  39. package/dist/assets/web/dist/assets/{ResidentDetailPage-CrfCd8fx.js → ResidentDetailPage-CUAOh5d9.js} +1 -1
  40. package/dist/assets/web/dist/assets/ResidentsIndexPage-B3KUlYy2.js +1 -0
  41. package/dist/assets/web/dist/assets/RunFoldRow-Dulcbg25.js +1 -0
  42. package/dist/assets/web/dist/assets/RunRoutePage-DGoGrQZq.js +6 -0
  43. package/dist/assets/web/dist/assets/RunsIndexPage-37W7OZbC.js +1 -0
  44. package/dist/assets/web/dist/assets/{RunsTabs-XpCqeWu4.js → RunsTabs-BnkvCBrM.js} +1 -1
  45. package/dist/assets/web/dist/assets/ScheduledPage-C1y9jgAk.js +1 -0
  46. package/dist/assets/web/dist/assets/SettingsPage-ggfB-Rqr.js +1 -0
  47. package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +1 -0
  48. package/dist/assets/web/dist/assets/Slideover-CX-LEklF.js +1 -0
  49. package/dist/assets/web/dist/assets/{StatusDot-DkXALvKf.js → StatusDot-CiByEtoR.js} +1 -1
  50. package/dist/assets/web/dist/assets/{Tooltip-DtLIyi5P.js → Tooltip-DyRxk8A8.js} +1 -1
  51. package/dist/assets/web/dist/assets/UnitRoutePage-CnWyOizV.js +1 -0
  52. package/dist/assets/web/dist/assets/{angular-html-DYtV9uIN.js → angular-html-DAQ_1T1v.js} +1 -1
  53. package/dist/assets/web/dist/assets/{angular-ts-DF2P25Zp.js → angular-ts-D4kxRVp1.js} +1 -1
  54. package/dist/assets/web/dist/assets/{apl-Ff96wxko.js → apl-Dy07l64y.js} +1 -1
  55. package/dist/assets/web/dist/assets/{astro-LbBnZna-.js → astro-DZIbTlc7.js} +1 -1
  56. package/dist/assets/web/dist/assets/{blade-DltuxeBW.js → blade-CmSXWX3l.js} +1 -1
  57. package/dist/assets/web/dist/assets/{c-DBg7yuPx.js → c-ggzVdJLK.js} +1 -1
  58. package/dist/assets/web/dist/assets/{chapel-C83hUAds.js → chapel-Bo1wktsG.js} +1 -1
  59. package/dist/assets/web/dist/assets/{cobol-DB0yV_2D.js → cobol-DJUmtKM2.js} +1 -1
  60. package/dist/assets/web/dist/assets/{coffee-Bpih6Kzl.js → coffee-BZWOVz4C.js} +1 -1
  61. package/dist/assets/web/dist/assets/{cpp-CU00ZeRn.js → cpp-Dtk39yMw.js} +1 -1
  62. package/dist/assets/web/dist/assets/{crystal-f8ImJkux.js → crystal-CGf_-Zoj.js} +1 -1
  63. package/dist/assets/web/dist/assets/{css-4VrC4ISo.js → css-CX2fcL4u.js} +1 -1
  64. package/dist/assets/web/dist/assets/delivery-CTdOqpVK.js +1 -0
  65. package/dist/assets/web/dist/assets/{dist-DtA7JiH5.js → dist-D8TaC961.js} +2 -2
  66. package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +1 -0
  67. package/dist/assets/web/dist/assets/{edge-BwFDzfS_.js → edge-BYS8FtgQ.js} +1 -1
  68. package/dist/assets/web/dist/assets/{elixir-wwIYIpJm.js → elixir-fhUpi3nO.js} +1 -1
  69. package/dist/assets/web/dist/assets/{elm-DyDuM3v3.js → elm-DAQNiwsN.js} +1 -1
  70. package/dist/assets/web/dist/assets/{erb-DFJBEVrh.js → erb-CWzEys6U.js} +1 -1
  71. package/dist/assets/web/dist/assets/eventSource-tRprhqm7.js +1 -0
  72. package/dist/assets/web/dist/assets/favicon-D1hkyC5j.js +1 -0
  73. package/dist/assets/web/dist/assets/{git-rebase-CuDv-44X.js → git-rebase-9rY3p-Rw.js} +1 -1
  74. package/dist/assets/web/dist/assets/{glimmer-js-ZFnrm4J3.js → glimmer-js-tY7hy0nE.js} +1 -1
  75. package/dist/assets/web/dist/assets/{glimmer-ts-XY4Yqshp.js → glimmer-ts-CTrLv5JF.js} +1 -1
  76. package/dist/assets/web/dist/assets/{glsl-xnR8efx0.js → glsl-BRJbiDfq.js} +1 -1
  77. package/dist/assets/web/dist/assets/{graphql-D-gO5tnP.js → graphql-B-MtVLh2.js} +1 -1
  78. package/dist/assets/web/dist/assets/{hack-CSxbW4ax.js → hack-BtIkOypQ.js} +1 -1
  79. package/dist/assets/web/dist/assets/{haml-D4ar42QT.js → haml-CoOH6gfa.js} +1 -1
  80. package/dist/assets/web/dist/assets/{handlebars-CMLbDE7o.js → handlebars-DgrRu-GP.js} +1 -1
  81. package/dist/assets/web/dist/assets/{html-CVekU4Fs.js → html-DZaaI7ca.js} +1 -1
  82. package/dist/assets/web/dist/assets/{html-derivative-St3HHw_N.js → html-derivative-DgHvT-qZ.js} +1 -1
  83. package/dist/assets/web/dist/assets/{http-Mon0DWHS.js → http-CvAd51yz.js} +1 -1
  84. package/dist/assets/web/dist/assets/{hurl-Cbl5IvBs.js → hurl-CXGqAfzx.js} +1 -1
  85. package/dist/assets/web/dist/assets/{localIso-CNA-bhS-.js → indexFormat-B-pd4r7Y.js} +1 -1
  86. package/dist/assets/web/dist/assets/{indexRow-FLJu5mR9.js → indexRow-Cse3JGb0.js} +1 -1
  87. package/dist/assets/web/dist/assets/{java-Bprck1l-.js → java-CIQStIXF.js} +1 -1
  88. package/dist/assets/web/dist/assets/{javascript-C73G1-UR.js → javascript-WbH8s7E3.js} +1 -1
  89. package/dist/assets/web/dist/assets/{jinja-C4DuRxYJ.js → jinja-DzyWEVKC.js} +1 -1
  90. package/dist/assets/web/dist/assets/{jison-B2U03GEC.js → jison-CrT-zCa-.js} +1 -1
  91. package/dist/assets/web/dist/assets/{json-CvTZUDzx.js → json-lHjpjXMQ.js} +1 -1
  92. package/dist/assets/web/dist/assets/{jsx-DMWKCzwo.js → jsx-WTwYCHW6.js} +1 -1
  93. package/dist/assets/web/dist/assets/{julia-Czn7bV_3.js → julia-Bqpk9H7E.js} +1 -1
  94. package/dist/assets/web/dist/assets/{just-BBxpNfvC.js → just-LdXk1r3m.js} +1 -1
  95. package/dist/assets/web/dist/assets/{latex-Bw9yg5mr.js → latex-h_y2A5KD.js} +1 -1
  96. package/dist/assets/web/dist/assets/{liquid-BRCjaydE.js → liquid-CY7bF2_p.js} +1 -1
  97. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +1 -0
  98. package/dist/assets/web/dist/assets/{lua-DUu2KIC0.js → lua-oQ7XVZOY.js} +1 -1
  99. package/dist/assets/web/dist/assets/main-CGO6fQ8k.js +28 -0
  100. package/dist/assets/web/dist/assets/main-O8UXRuEv.css +1 -0
  101. package/dist/assets/web/dist/assets/{marko-CZJZIlSQ.js → marko-zv_qZ4QV.js} +1 -1
  102. package/dist/assets/web/dist/assets/{mdc-BmClq6fd.js → mdc-Dl4kL_YK.js} +1 -1
  103. package/dist/assets/web/dist/assets/{nginx-Bmt3xSEm.js → nginx-CAHCZj4F.js} +1 -1
  104. package/dist/assets/web/dist/assets/{nim-Da-i0utK.js → nim-BOtmwCJH.js} +1 -1
  105. package/dist/assets/web/dist/assets/{org-BvAre2qP.js → org-DspQk3HQ.js} +1 -1
  106. package/dist/assets/web/dist/assets/{perl-v9WwFFtB.js → perl-CAXmvVl0.js} +1 -1
  107. package/dist/assets/web/dist/assets/{php-DTU4Zyze.js → php-CMUx4VyM.js} +1 -1
  108. package/dist/assets/web/dist/assets/{pug-DhTwEqTO.js → pug-B55xVJTv.js} +1 -1
  109. package/dist/assets/web/dist/assets/{qml-BUD9AC_c.js → qml-DsNKCCRk.js} +1 -1
  110. package/dist/assets/web/dist/assets/{r-CAnu56n7.js → r-DbhP0VvO.js} +1 -1
  111. package/dist/assets/web/dist/assets/{razor-Dn0P4gAq.js → razor-BCpE6P-p.js} +1 -1
  112. package/dist/assets/web/dist/assets/{regexp-CZyBRaVW.js → regexp-BllsjiN4.js} +1 -1
  113. package/dist/assets/web/dist/assets/{rst-Ckyd5UsY.js → rst-CO5Rpq9V.js} +1 -1
  114. package/dist/assets/web/dist/assets/{ruby-BBQ62fsx.js → ruby-C2A4OgQq.js} +1 -1
  115. package/dist/assets/web/dist/assets/{sas-CW35hMxG.js → sas-Butzoyql.js} +1 -1
  116. package/dist/assets/web/dist/assets/{scss-CpV8pF4J.js → scss-PEUwJBpf.js} +1 -1
  117. package/dist/assets/web/dist/assets/settingsApi-priOPiwu.js +1 -0
  118. package/dist/assets/web/dist/assets/{shellscript-CRJvsmE1.js → shellscript-3Yhq0khE.js} +1 -1
  119. package/dist/assets/web/dist/assets/{shellsession-CyriUZxf.js → shellsession-04Hkb7m3.js} +1 -1
  120. package/dist/assets/web/dist/assets/{soy-D5QzYUeg.js → soy-BnId73cP.js} +1 -1
  121. package/dist/assets/web/dist/assets/{sql-CUBf_Mse.js → sql-BKwdc0ch.js} +1 -1
  122. package/dist/assets/web/dist/assets/sseReplay-DutHNghE.js +9 -0
  123. package/dist/assets/web/dist/assets/{stata-B-4n4ITz.js → stata-BPPtNkay.js} +1 -1
  124. package/dist/assets/web/dist/assets/{surrealql-RuWZBLtr.js → surrealql-CDPChcYP.js} +1 -1
  125. package/dist/assets/web/dist/assets/{svelte-CO5YzCsK.js → svelte-B15DIQ-z.js} +1 -1
  126. package/dist/assets/web/dist/assets/{templ-DbkWVaBF.js → templ-D-rp_AiB.js} +1 -1
  127. package/dist/assets/web/dist/assets/{tex-IuxoEcId.js → tex-BnVDCD7p.js} +1 -1
  128. package/dist/assets/web/dist/assets/{ts-tags-g6dGQPjd.js → ts-tags-cpujdyAR.js} +1 -1
  129. package/dist/assets/web/dist/assets/{tsx-WnXwiyH2.js → tsx-D1vO4L72.js} +1 -1
  130. package/dist/assets/web/dist/assets/{twig-Lx1G7PnY.js → twig-oQGnQGRc.js} +1 -1
  131. package/dist/assets/web/dist/assets/{typescript-BQd7biLF.js → typescript-CKSFpXiH.js} +1 -1
  132. package/dist/assets/web/dist/assets/{typst-D2lXPHK7.js → typst-DKHFo8BF.js} +1 -1
  133. package/dist/assets/web/dist/assets/{vue-4wVQD9y9.js → vue-BstokpiS.js} +1 -1
  134. package/dist/assets/web/dist/assets/{vue-html-DZPaRte4.js → vue-html-DpCp0_Sz.js} +1 -1
  135. package/dist/assets/web/dist/assets/{vue-vine-DbmZCmu-.js → vue-vine-y5d5wUb1.js} +1 -1
  136. package/dist/assets/web/dist/assets/{xml-DV16szyo.js → xml-Br2hx5Ax.js} +1 -1
  137. package/dist/assets/web/dist/assets/{xsl-eo5-1sk0.js → xsl-Bc8Cobap.js} +1 -1
  138. package/dist/assets/web/dist/assets/{yaml-BXdX8HZf.js → yaml-LOITy_I1.js} +1 -1
  139. package/dist/cli.js +2586 -1603
  140. package/package.json +1 -1
  141. package/dist/assets/web/dist/assets/AppShell-MYHNxJks.js +0 -1
  142. package/dist/assets/web/dist/assets/CostsPage-DKk9OjB0.js +0 -2
  143. package/dist/assets/web/dist/assets/DeliveryPage-Kz6js49i.js +0 -1
  144. package/dist/assets/web/dist/assets/NotFoundPage-BXLrQQzy.js +0 -1
  145. package/dist/assets/web/dist/assets/ResidentsIndexPage-BdW0k2h3.js +0 -1
  146. package/dist/assets/web/dist/assets/RunFoldRow-CbTd9SiS.js +0 -9
  147. package/dist/assets/web/dist/assets/RunRoutePage-br0H-8Pz.js +0 -6
  148. package/dist/assets/web/dist/assets/RunsIndexPage-Bnzb15zH.js +0 -1
  149. package/dist/assets/web/dist/assets/ScheduledPage-NLhjzHAz.js +0 -1
  150. package/dist/assets/web/dist/assets/SettingsPage-D-i0uVw-.js +0 -1
  151. package/dist/assets/web/dist/assets/UnitRoutePage-DGXukEAH.js +0 -1
  152. package/dist/assets/web/dist/assets/durationTone-CYMux25J.js +0 -1
  153. package/dist/assets/web/dist/assets/favicon-CSt-qDvc.js +0 -1
  154. package/dist/assets/web/dist/assets/main-CFHF2RvQ.css +0 -1
  155. package/dist/assets/web/dist/assets/main-Dht0in4v.js +0 -28
@@ -323,6 +323,9 @@ workspaceDir: ./workspaces
323
323
  # ship:
324
324
  # maxRounds: 3 # review rounds per unit
325
325
  # maxMinutes: 120 # the ship preset's wall-clock budget in minutes (the registry's default)
326
+ # addressSeverity: minor # the severity an approve's findings are held to (blocking|major|minor|nit);
327
+ # # a finding at or above it sends the round into a fix step instead of merge-ready.
328
+ # # Per channel/user: config set … --ship.addressSeverity; per run: severity:<level>.
326
329
 
327
330
  # `harness` is a retired key (docs/reference/specs/harness-pi.md item 1). It
328
331
  # moved the presets onto pi one at a time; every preset runs on pi now — the pi
@@ -386,6 +389,9 @@ workspaceDir: ./workspaces
386
389
  # cloudflareAccountName: acme-infra # optional: how the page names the account beside its share (else the id's first hex)
387
390
  # cloudflareTokenEnv: CF_ANALYTICS_TOKEN # token with Account Analytics:Read
388
391
  # anthropicAdminKeyEnv: ANTHROPIC_ADMIN_KEY # optional: sk-ant-admin… for LLM spend
392
+ # snapshot: # the page serves a snapshot of both billing sources, never a live read;
393
+ # everyHours: 24 # how often the bot reads them (1–168, default 24). `costs snapshot`
394
+ # # takes one now. Kept on the state Worker any `*.worker` block names
389
395
  # groups:
390
396
  # myapp:
391
397
  # label: My App
@@ -10,6 +10,7 @@ import {
10
10
  } from "../../src/core/memory/engine.ts";
11
11
  import { tokenize } from "../../src/core/memory/scorer.ts";
12
12
  import { FIRING_DETAIL_MAX, isScheduleFiring, type ScheduleFiring } from "../../src/core/schedules.ts";
13
+ import { isCostsSnapshot, type CostsSnapshot } from "../../src/core/costsSnapshotStore.ts";
13
14
  import { REPO_SLUG } from "../../src/core/delivery.ts";
14
15
  import {
15
16
  isDeliverySnapshot,
@@ -176,6 +177,8 @@ export interface Env {
176
177
  SESSION_LOGS: DurableObjectNamespace<SessionLogDO>;
177
178
  /** Delivery snapshots (delivery item 10): ONE DeliveryDO (named "delivery"), one snapshot per repository. */
178
179
  DELIVERY: DurableObjectNamespace<DeliveryDO>;
180
+ /** The costs snapshot (costs item 6): ONE CostsSnapshotDO (named "costs"), one snapshot per installation. */
181
+ COSTS: DurableObjectNamespace<CostsSnapshotDO>;
179
182
  /** The ship coordinator Workflow in the bot's shim Worker (run-history item
180
183
  * 47): where `RunHistoryDO.finish` sends `run-finished-<runId>` for a record
181
184
  * carrying `parentInstanceId`. Optional: this Worker deploys without it (the
@@ -188,6 +191,9 @@ export interface Env {
188
191
  /** The single DeliveryDO's name — every repository's snapshot lives in one object. */
189
192
  const DELIVERY_OBJECT = "delivery";
190
193
 
194
+ /** The single CostsSnapshotDO's name — the installation has one costs snapshot. */
195
+ const COSTS_OBJECT = "costs";
196
+
191
197
  /** The single ConfigDO's name. */
192
198
  const CONFIG_OBJECT = "config";
193
199
  /** A config document key: short, lowercase, like `overrides`. */
@@ -929,6 +935,110 @@ async function handleDelivery(pathname: string, body: unknown, env: Env): Promis
929
935
  return json({ error: "not found" }, 404);
930
936
  }
931
937
 
938
+ // ---------------------------------------------------------------------------
939
+ // The costs snapshot (docs/reference/specs/costs.md item 6)
940
+ // ---------------------------------------------------------------------------
941
+
942
+ /** The datasets a snapshot's `usage` carries, each stored as its own row. */
943
+ const USAGE_PARTS = [
944
+ "containers",
945
+ "durableObjectRequests",
946
+ "durableObjectDays",
947
+ "durableObjectStorage",
948
+ "workers",
949
+ "r2Storage",
950
+ "r2Operations",
951
+ "workflows",
952
+ ] as const;
953
+
954
+ /**
955
+ * The installation's one costs snapshot: both billing sources' rows over the
956
+ * page's widest range plus the run history's per-user usage, and when they
957
+ * were read (`CostsSnapshot`, the shape the bot validates too). Stored as one
958
+ * row per part — the meta, each usage dataset, the LLM rows, the run usage —
959
+ * so no single value nears the row limit as the window's rows grow; replaced
960
+ * whole on every put, in one transaction, and read back as one document.
961
+ */
962
+ export class CostsSnapshotDO extends DurableObject<Env> {
963
+ private readonly sql: SqlStorage;
964
+
965
+ constructor(ctx: DurableObjectState, env: Env) {
966
+ super(ctx, env);
967
+ this.sql = ctx.storage.sql;
968
+ this.sql.exec(`
969
+ CREATE TABLE IF NOT EXISTS parts (
970
+ part TEXT PRIMARY KEY,
971
+ body TEXT NOT NULL
972
+ );
973
+ `);
974
+ }
975
+
976
+ /** Replace the snapshot whole: every part rewritten in one transaction. */
977
+ async put(snapshot: CostsSnapshot): Promise<void> {
978
+ const { usage, llm, runUsage, ...meta } = snapshot;
979
+ const rows: Array<[string, unknown]> = [
980
+ ["meta", meta],
981
+ ...USAGE_PARTS.map((name): [string, unknown] => [`usage.${name}`, usage[name]]),
982
+ ["llm", llm],
983
+ ["runUsage", runUsage],
984
+ ];
985
+ this.ctx.storage.transactionSync(() => {
986
+ this.sql.exec(`DELETE FROM parts`);
987
+ for (const [part, body] of rows)
988
+ this.sql.exec(`INSERT INTO parts (part, body) VALUES (?, ?)`, part, JSON.stringify(body));
989
+ });
990
+ }
991
+
992
+ /** The stored snapshot reassembled from its parts, or null when none was stored (or the parts do not make one). */
993
+ async get(): Promise<CostsSnapshot | null> {
994
+ const parts = new Map(
995
+ this.sql
996
+ .exec<{ part: string; body: string }>(`SELECT part, body FROM parts`)
997
+ .toArray()
998
+ .map((r) => [r.part, r.body]),
999
+ );
1000
+ const meta = parts.get("meta");
1001
+ if (meta === undefined) return null;
1002
+ const read = (part: string): unknown => {
1003
+ const body = parts.get(part);
1004
+ return body === undefined ? undefined : (JSON.parse(body) as unknown);
1005
+ };
1006
+ const usage = Object.fromEntries(USAGE_PARTS.map((name) => [name, read(`usage.${name}`)]));
1007
+ const snapshot = {
1008
+ ...(JSON.parse(meta) as Record<string, unknown>),
1009
+ usage,
1010
+ llm: read("llm"),
1011
+ runUsage: read("runUsage"),
1012
+ };
1013
+ return isCostsSnapshot(snapshot) ? snapshot : null;
1014
+ }
1015
+ }
1016
+
1017
+ const COSTS_ROUTES = new Set(["/costs/snapshot/get", "/costs/snapshot/put"]);
1018
+
1019
+ async function handleCosts(pathname: string, body: unknown, env: Env): Promise<Response> {
1020
+ const b = (typeof body === "object" && body !== null ? body : {}) as Record<string, unknown>;
1021
+ const dO = env.COSTS.get(env.COSTS.idFromName(COSTS_OBJECT));
1022
+ if (pathname === "/costs/snapshot/get") {
1023
+ const snapshot = await dO.get();
1024
+ console.log(
1025
+ `[costs/snapshot/get] -> ${snapshot ? `snapshot taken ${snapshot.takenAt} by ${snapshot.takenBy}` : "none"}`,
1026
+ );
1027
+ return json({ snapshot });
1028
+ }
1029
+ if (pathname === "/costs/snapshot/put") {
1030
+ if (!isCostsSnapshot(b.snapshot))
1031
+ return json(
1032
+ { error: "snapshot must be a CostsSnapshot (takenAt, takenBy, durationMs, range, usage, llm, runUsage)" },
1033
+ 400,
1034
+ );
1035
+ await dO.put(b.snapshot);
1036
+ console.log(`[costs/snapshot/put] <- snapshot taken ${b.snapshot.takenAt} by ${b.snapshot.takenBy}`);
1037
+ return json({ ok: true });
1038
+ }
1039
+ return json({ error: "not found" }, 404);
1040
+ }
1041
+
932
1042
  /** Documents are small; a body over this is refused before storage. */
933
1043
  const MAX_CONFIG_DOCUMENT_BYTES = 256 * 1024;
934
1044
 
@@ -3229,6 +3339,8 @@ const MAX_SNAPSHOT_BODY_BYTES = 16 * 1024 * 1024;
3229
3339
  function bodyFenceFor(pathname: string): number {
3230
3340
  if (WIDE_BODY_ROUTES.has(pathname)) return MAX_RUN_PUT_BODY_BYTES;
3231
3341
  if (pathname === "/delivery/put" || pathname === "/delivery/merge") return MAX_SNAPSHOT_BODY_BYTES;
3342
+ // A ninety-day costs snapshot is a few hundred KB today and grows with the account's rows.
3343
+ if (pathname === "/costs/snapshot/put") return MAX_SNAPSHOT_BODY_BYTES;
3232
3344
  return MAX_BODY_BYTES;
3233
3345
  }
3234
3346
 
@@ -3707,6 +3819,7 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
3707
3819
  const ROUTES = new Set([
3708
3820
  ...CONFIG_ROUTES,
3709
3821
  ...DELIVERY_ROUTES,
3822
+ ...COSTS_ROUTES,
3710
3823
  "/retrieve",
3711
3824
  "/write",
3712
3825
  "/list",
@@ -3735,7 +3848,7 @@ interface Admission {
3735
3848
  async function handleRequest(request: Request, env: Env, admission: Admission): Promise<Response> {
3736
3849
  const url = new URL(request.url);
3737
3850
  if (url.pathname === "/healthz" && request.method === "GET")
3738
- return json({ ok: true, build: BUILD, features: ["memory", "schedules", "runs", "config", "delivery"] });
3851
+ return json({ ok: true, build: BUILD, features: ["memory", "schedules", "runs", "config", "delivery", "costs"] });
3739
3852
  if (!admission.known) return json({ error: "not found" }, 404);
3740
3853
  if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
3741
3854
  if (!admission.authorized) return json({ error: "unauthorized" }, 401);
@@ -3787,6 +3900,7 @@ async function handleRequest(request: Request, env: Env, admission: Admission):
3787
3900
  if (url.pathname.startsWith("/runs/")) return handleRuns(url.pathname, body, env);
3788
3901
  if (url.pathname.startsWith("/config/")) return handleConfig(url.pathname, body, env);
3789
3902
  if (url.pathname.startsWith("/delivery/")) return handleDelivery(url.pathname, body, env);
3903
+ if (url.pathname.startsWith("/costs/")) return handleCosts(url.pathname, body, env);
3790
3904
 
3791
3905
  if (url.pathname === "/retrieve") {
3792
3906
  const parsed = parseRetrieve(body);
@@ -36,6 +36,11 @@
36
36
  // repository — the merged pull requests' facts over the window, one row
37
37
  // each, and when they were read (docs/reference/specs/delivery.md item 10).
38
38
  { "name": "DELIVERY", "class_name": "DeliveryDO" },
39
+ // ONE CostsSnapshotDO (named "costs"): the installation's costs snapshot —
40
+ // both billing sources' rows over the costs page's widest range and the run
41
+ // history's per-user usage, one row per part, replaced whole on every take
42
+ // (docs/reference/specs/costs.md item 6).
43
+ { "name": "COSTS", "class_name": "CostsSnapshotDO" },
39
44
  // One SessionLogDO per session — a thread and an agent (the DO name is
40
45
  // `<threadKey>:<agent>`): the transcript rows of every run of the
41
46
  // session, a full-text index over them, and the notepad
@@ -74,6 +79,7 @@
74
79
  // retired with its rows — docs/reference/specs/self-improvement.md item 1.
75
80
  { "tag": "v7", "deleted_classes": ["FrictionDO"] },
76
81
  { "tag": "v8", "new_sqlite_classes": ["DeliveryDO"] },
77
- { "tag": "v9", "new_sqlite_classes": ["SessionLogDO"] }
82
+ { "tag": "v9", "new_sqlite_classes": ["SessionLogDO"] },
83
+ { "tag": "v10", "new_sqlite_classes": ["CostsSnapshotDO"] }
78
84
  ]
79
85
  }