@coreplane/switchboard 1.251.0 → 1.253.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 (167) hide show
  1. package/dist/assets/config/config.example.yaml +28 -2
  2. package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
  3. package/dist/assets/deploy/cloudflare/worker.ts +6 -3
  4. package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +223 -38
  6. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
  7. package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
  8. package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
  9. package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
  10. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
  11. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
  12. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
  13. package/dist/assets/deploy/profile.example.json +1 -1
  14. package/dist/assets/package-lock.json +3 -3
  15. package/dist/assets/package.json +3 -2
  16. package/dist/assets/project.json +13 -9
  17. package/dist/assets/source.json +3 -3
  18. package/dist/assets/src/agents/registry.ts +5 -5
  19. package/dist/assets/src/core/authz/policy.ts +8 -0
  20. package/dist/assets/src/core/budgets.ts +39 -0
  21. package/dist/assets/src/core/coordinator/contract.ts +93 -1
  22. package/dist/assets/src/core/coordinator/driver.ts +236 -21
  23. package/dist/assets/src/core/drain.ts +50 -0
  24. package/dist/assets/src/core/memory/engine.ts +123 -4
  25. package/dist/assets/src/core/memory/scorer.ts +12 -4
  26. package/dist/assets/src/core/memory/types.ts +73 -12
  27. package/dist/assets/src/core/modelCard.ts +32 -4
  28. package/dist/assets/src/core/modelPricing.ts +111 -1
  29. package/dist/assets/src/core/modelProxy/usage.ts +88 -0
  30. package/dist/assets/src/core/modelRegistry.ts +15 -1
  31. package/dist/assets/src/core/refusal.ts +4 -7
  32. package/dist/assets/src/core/reviewVerdict.ts +4 -0
  33. package/dist/assets/src/core/runEvents.ts +82 -3
  34. package/dist/assets/src/core/runFriction.ts +8 -2
  35. package/dist/assets/src/core/runLedger/types.ts +11 -0
  36. package/dist/assets/src/core/runMetrics.ts +237 -0
  37. package/dist/assets/src/core/runRecord.ts +46 -0
  38. package/dist/assets/src/core/runUsage.ts +67 -13
  39. package/dist/assets/src/core/schedules.ts +3 -0
  40. package/dist/assets/src/core/ship/contract.ts +34 -14
  41. package/dist/assets/src/core/ship/coordinator.ts +485 -55
  42. package/dist/assets/src/core/ship/renewal.ts +10 -5
  43. package/dist/assets/src/core/trace/attrs.ts +24 -0
  44. package/dist/assets/src/core/types.ts +5 -5
  45. package/dist/assets/src/core/verbosity.ts +48 -0
  46. package/dist/assets/src/deploy/liveGate.ts +40 -13
  47. package/dist/assets/src/deploy/profile.ts +16 -0
  48. package/dist/assets/src/deploy/restart.ts +11 -12
  49. package/dist/assets/src/execution/residentDepCache.ts +50 -1
  50. package/dist/assets/src/execution/residentDepsStore.ts +40 -2
  51. package/dist/assets/src/execution/residentRefresh.ts +55 -3
  52. package/dist/assets/src/execution/residentSteps.ts +4 -0
  53. package/dist/assets/src/execution/sandboxErrors.ts +8 -0
  54. package/dist/assets/web/dist/.vite/manifest.json +444 -430
  55. package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
  56. package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
  57. package/dist/assets/web/dist/assets/{DeliveryPage-DUXd-Sl-.js → DeliveryPage-C7jaEJip.js} +1 -1
  58. package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
  59. package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
  60. package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
  61. package/dist/assets/web/dist/assets/{PendingTurnRow-DDhMhrI7.js → PendingTurnRow-C-pUgyyX.js} +1 -1
  62. package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
  63. package/dist/assets/web/dist/assets/{ResidentDetailPage-BnEoOnGQ.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
  64. package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
  65. package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
  66. package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
  67. package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
  68. package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
  69. package/dist/assets/web/dist/assets/{ScheduledPage-B_GgeJrb.js → ScheduledPage-C8FYoZy5.js} +1 -1
  70. package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
  71. package/dist/assets/web/dist/assets/{SettingsPage-BXX4R113.js → SettingsPage-CkYIhLUl.js} +1 -1
  72. package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-C0TUgnC1.js} +1 -1
  73. package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-CN5cZdl3.js} +1 -1
  74. package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.js → UnitRoutePage-D_EtAl8r.js} +1 -1
  75. package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
  76. package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
  77. package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
  78. package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
  79. package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
  80. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
  81. package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
  82. package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
  83. package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
  84. package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
  85. package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
  86. package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
  87. package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
  88. package/dist/assets/web/dist/assets/{dist-BCVXeBJ9.js → dist-B1Mf2JbM.js} +2 -2
  89. package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
  90. package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
  91. package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
  92. package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
  93. package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
  94. package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
  95. package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
  96. package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
  97. package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
  98. package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
  99. package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
  100. package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
  101. package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
  102. package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
  103. package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
  104. package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
  105. package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
  106. package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
  107. package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
  108. package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
  109. package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
  110. package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
  111. package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
  112. package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
  113. package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
  114. package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
  115. package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
  116. package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
  117. package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
  118. package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
  119. package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
  120. package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
  121. package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
  122. package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
  123. package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
  124. package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
  125. package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
  126. package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
  127. package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
  128. package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
  129. package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
  130. package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
  131. package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
  132. package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
  133. package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
  134. package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
  135. package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
  136. package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
  137. package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
  138. package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
  139. package/dist/assets/web/dist/assets/sseReplay-BYo2EZKy.js +11 -0
  140. package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
  141. package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
  142. package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
  143. package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
  144. package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
  145. package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
  146. package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
  147. package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
  148. package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
  149. package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
  150. package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
  151. package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
  152. package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
  153. package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
  154. package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
  155. package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
  156. package/dist/cli.js +5623 -2455
  157. package/package.json +1 -1
  158. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
  159. package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
  160. package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
  161. package/dist/assets/web/dist/assets/RunRoutePage-9klVWhSF.js +0 -9
  162. package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
  163. package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
  164. package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
  165. package/dist/assets/web/dist/assets/main-Dkcbtu3u.js +0 -28
  166. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
  167. package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +0 -9
@@ -0,0 +1,237 @@
1
+ import { FRICTION_CATEGORIES, type CategoryTotals, type FrictionCategory } from "./runFriction.js";
2
+ import { llmUsdOfUsage, type ModelPriceTable } from "./modelPricing.js";
3
+ import { emptyUsage, UNKNOWN_AGENT } from "./runUsage.js";
4
+ import { utf8ByteLength, type RunRecord } from "./runRecord.js";
5
+
6
+ // Run metrics (docs/reference/specs/run-metrics.md; docs/decisions/0063-every-finished-run-writes-one-metrics-point-and-a-metrics-page-reads-the-trend.md):
7
+ // one flat point per finished run, written to a Workers Analytics Engine
8
+ // dataset beside the record's commit. This file is the point's contract, shared
9
+ // by both sides of the wire — the bot's store/ledger clients compute the point
10
+ // (`pointOf`), the state Worker validates it (`isRunMetricsPoint`) and decides
11
+ // whether the row turned final (`pointTurnsFinal`) — so it is node-free like
12
+ // `runRecord.ts`: no Node built-ins, no I/O, no clock.
13
+
14
+ /** A point as the platform's `writeDataPoint` takes it: one index (the
15
+ * sampling key), positional string blobs and positional doubles. */
16
+ export interface RunMetricsPoint {
17
+ indexes: [string];
18
+ blobs: string[];
19
+ doubles: number[];
20
+ }
21
+
22
+ /** The platform's byte cap per blob (and per index); `pointOf` truncates. */
23
+ export const MAX_POINT_BLOB_BYTES = 96;
24
+
25
+ /** The point's schema word, `blob1` — bump it when a position changes meaning. */
26
+ export const POINT_SCHEMA = "1";
27
+
28
+ const BLOB_NAMES = [
29
+ "schema",
30
+ "agent",
31
+ "preset",
32
+ "model",
33
+ "status",
34
+ "failure kind",
35
+ "dominant friction",
36
+ "channel",
37
+ "repository",
38
+ "machine class",
39
+ "route class",
40
+ "reply",
41
+ "lineage",
42
+ "requester",
43
+ "identity",
44
+ "run id",
45
+ ] as const;
46
+
47
+ const DOUBLE_NAMES = [
48
+ "wall",
49
+ "getting ready",
50
+ "thinking",
51
+ "tools",
52
+ "finishing up",
53
+ "overhead",
54
+ "not recorded",
55
+ "not loaded",
56
+ "turns",
57
+ "input tokens",
58
+ "output tokens",
59
+ "cache read tokens",
60
+ "cache write tokens",
61
+ "dollars",
62
+ "steps",
63
+ "tool calls",
64
+ "events",
65
+ "unpriced tokens",
66
+ "minutes",
67
+ "finished at",
68
+ ] as const;
69
+
70
+ export type PointBlobName = (typeof BLOB_NAMES)[number];
71
+ export type PointDoubleName = (typeof DOUBLE_NAMES)[number];
72
+
73
+ /** The ONE ordered table naming every position once: `blobs[i]` is the meaning
74
+ * of `blob<i+1>`, `doubles[i]` of `double<i+1>`, `index[0]` of `index1`. The
75
+ * writer builds from it (`pointOf` fills a `Record<name, value>` and maps it
76
+ * through this order, so a position added here without a value fails to
77
+ * compile) and the reader's SQL aliases from it — a query names a column
78
+ * through its name here, never a magic number. */
79
+ export const POINT_COLUMNS = {
80
+ index: ["agent"] as const,
81
+ blobs: BLOB_NAMES,
82
+ doubles: DOUBLE_NAMES,
83
+ };
84
+
85
+ /** The 1-based platform column of a named blob: `"status"` → `"blob5"`. */
86
+ export function blobColumn(name: PointBlobName): string {
87
+ return `blob${POINT_COLUMNS.blobs.indexOf(name) + 1}`;
88
+ }
89
+
90
+ /** The 1-based platform column of a named double: `"dollars"` → `"double14"`. */
91
+ export function doubleColumn(name: PointDoubleName): string {
92
+ return `double${POINT_COLUMNS.doubles.indexOf(name) + 1}`;
93
+ }
94
+
95
+ /** A point's blob by its `POINT_COLUMNS` name — the tests' and readers' accessor. */
96
+ export function blobOf(point: RunMetricsPoint, name: PointBlobName): string {
97
+ return point.blobs[POINT_COLUMNS.blobs.indexOf(name)];
98
+ }
99
+
100
+ /** A point's double by its `POINT_COLUMNS` name. */
101
+ export function doubleOf(point: RunMetricsPoint, name: PointDoubleName): number {
102
+ return point.doubles[POINT_COLUMNS.doubles.indexOf(name)];
103
+ }
104
+
105
+ /** Truncate to the platform's per-blob byte cap (multi-byte safe: cut by
106
+ * characters until the UTF-8 size fits — blob values are ids and enum words,
107
+ * so the loop is theoretical). */
108
+ function capBlob(s: string): string {
109
+ let out = s;
110
+ while (utf8ByteLength(out) > MAX_POINT_BLOB_BYTES) out = out.slice(0, -1);
111
+ return out;
112
+ }
113
+
114
+ const isFinite_ = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
115
+
116
+ /** The dominant friction category of a stored diagnosis: the `FrictionCategory`
117
+ * with the largest `durationMs` among those with a count (first in
118
+ * `FRICTION_CATEGORIES` order on a tie); empty when no category has a count. */
119
+ export function dominantFriction(byCategory: Partial<Record<FrictionCategory, CategoryTotals>>): FrictionCategory | "" {
120
+ let best: FrictionCategory | "" = "";
121
+ let bestMs = -1;
122
+ for (const c of FRICTION_CATEGORIES) {
123
+ const t = byCategory[c];
124
+ if (!t || t.count <= 0) continue;
125
+ if (t.durationMs > bestMs) {
126
+ best = c;
127
+ bestMs = t.durationMs;
128
+ }
129
+ }
130
+ return best;
131
+ }
132
+
133
+ /**
134
+ * The record's metrics point, or `undefined` for a record still in its
135
+ * provisional window (`provisional: true`) — and for nothing else: a final
136
+ * record whose `finishedAt` equals its `startedAt` is counted with a zero
137
+ * wall. Reads ONLY the record's typed dimensions and counters — never
138
+ * `activity`, `label`, `events`, `verdict`, `handoff`, `dispositions` or
139
+ * `diagnosis.verdict` — so no free text reaches the dataset. Dollars are
140
+ * priced at finish through the same table `RunsService` prices reads with
141
+ * (`llmUsdOfUsage`); an unpriced model's tokens land in `unpriced tokens`,
142
+ * never in `dollars` as $0.
143
+ */
144
+ export function pointOf(record: RunRecord, prices?: ModelPriceTable): RunMetricsPoint | undefined {
145
+ if (record.provisional === true) return undefined;
146
+ const usage = record.usage ?? emptyUsage();
147
+ const priced = llmUsdOfUsage(usage, prices);
148
+ let inputTokens = 0;
149
+ let outputTokens = 0;
150
+ let cacheReadTokens = 0;
151
+ let cacheWriteTokens = 0;
152
+ for (const m of Object.values(usage.byModel)) {
153
+ inputTokens += m.inputTokens;
154
+ outputTokens += m.outputTokens;
155
+ cacheReadTokens += m.cacheReadTokens;
156
+ cacheWriteTokens += m.cacheWriteTokens;
157
+ }
158
+ const shape = record.diagnosis.shape;
159
+ const blobs: Record<PointBlobName, string> = {
160
+ schema: POINT_SCHEMA,
161
+ agent: record.agent ?? "",
162
+ preset: record.profile?.preset ?? "",
163
+ model: record.model ?? "",
164
+ status: record.status,
165
+ "failure kind": record.failure?.kind ?? "",
166
+ "dominant friction": dominantFriction(record.diagnosis.byCategory),
167
+ channel: record.channelId,
168
+ repository: record.repo ?? "",
169
+ "machine class": record.profile?.machine ?? "",
170
+ "route class": record.route !== undefined ? "routed" : "chosen",
171
+ reply: record.replyOk === true ? "ok" : record.replyOk === false ? "failed" : "none",
172
+ lineage: record.parentRunId !== undefined ? "child" : "root",
173
+ requester: record.userId,
174
+ identity: record.profile?.identity ?? "",
175
+ "run id": record.id,
176
+ };
177
+ const doubles: Record<PointDoubleName, number> = {
178
+ wall: record.finishedAt - (record.receivedAt ?? record.startedAt),
179
+ "getting ready": shape?.gettingReadyMs ?? 0,
180
+ thinking: shape?.thinkingMs ?? 0,
181
+ tools: shape?.toolsMs ?? 0,
182
+ "finishing up": shape?.finishingUpMs ?? 0,
183
+ overhead: shape?.overheadMs ?? 0,
184
+ "not recorded": shape?.notRecordedMs ?? 0,
185
+ "not loaded": shape?.notLoadedMs ?? 0,
186
+ turns: usage.turns,
187
+ "input tokens": inputTokens,
188
+ "output tokens": outputTokens,
189
+ "cache read tokens": cacheReadTokens,
190
+ "cache write tokens": cacheWriteTokens,
191
+ dollars: priced.usd,
192
+ steps: record.stepCount ?? 0,
193
+ "tool calls": isFinite_(record.diagnosis.toolCalls) ? record.diagnosis.toolCalls : 0,
194
+ events: record.eventCount,
195
+ "unpriced tokens": priced.unpricedTokens,
196
+ minutes: record.profile?.minutes ?? 0,
197
+ "finished at": record.finishedAt,
198
+ };
199
+ return {
200
+ indexes: [capBlob(record.agent ?? UNKNOWN_AGENT)],
201
+ blobs: POINT_COLUMNS.blobs.map((n) => capBlob(blobs[n])),
202
+ doubles: POINT_COLUMNS.doubles.map((n) => doubles[n]),
203
+ };
204
+ }
205
+
206
+ /** Structural check on a point from outside the process (the `/runs/put` and
207
+ * `/runs/finish` bodies): exactly one index, exactly `POINT_COLUMNS.blobs.length`
208
+ * strings each at most `MAX_POINT_BLOB_BYTES` bytes, exactly
209
+ * `POINT_COLUMNS.doubles.length` finite numbers — and nothing else. */
210
+ export function isRunMetricsPoint(v: unknown): v is RunMetricsPoint {
211
+ if (typeof v !== "object" || v === null) return false;
212
+ const p = v as Record<string, unknown>;
213
+ if (!Array.isArray(p.indexes) || p.indexes.length !== 1) return false;
214
+ const index = p.indexes[0];
215
+ if (typeof index !== "string" || utf8ByteLength(index) > MAX_POINT_BLOB_BYTES) return false;
216
+ if (!Array.isArray(p.blobs) || p.blobs.length !== POINT_COLUMNS.blobs.length) return false;
217
+ if (!p.blobs.every((b) => typeof b === "string" && utf8ByteLength(b) <= MAX_POINT_BLOB_BYTES)) return false;
218
+ if (!Array.isArray(p.doubles) || p.doubles.length !== POINT_COLUMNS.doubles.length) return false;
219
+ return p.doubles.every(isFinite_);
220
+ }
221
+
222
+ /**
223
+ * The emission rule's one question, answered inside the store's transaction:
224
+ * did this write turn the run's row final? True exactly when the stored record
225
+ * is final (`provisional` absent) and the existing row was absent or itself
226
+ * provisional. There is deliberately no `finishedAt > startedAt` term: a plain
227
+ * `put` of an `interrupted` record over no row is a run counted once, and a
228
+ * final rewrite over a final row (the review artifact, an identical retry) is
229
+ * false however its stamps compare.
230
+ */
231
+ export function pointTurnsFinal(
232
+ existing: { provisional?: boolean } | undefined,
233
+ stored: Pick<RunRecord, "provisional">,
234
+ ): boolean {
235
+ if (stored.provisional === true) return false;
236
+ return existing === undefined || existing.provisional === true;
237
+ }
@@ -184,6 +184,13 @@ export interface RunRecord {
184
184
  * reading the events. Absent for a preset a person, a scope or the default
185
185
  * chose, and on records written before it existed. */
186
186
  route?: RunRouteDecision;
187
+ /** The operator's shadow decision beside the routed request (record 0057;
188
+ * the one-door plan's operator unit; run-history item 60): what the operator
189
+ * bound for the same event, redacted like the receipt — never the message
190
+ * text — with the intake gate's verdict when the gate was present. The
191
+ * agreement row reads it from here; it follows the record's retention.
192
+ * Absent under `routing.operator: off` and on records written before it. */
193
+ operator?: RunOperatorDecision;
187
194
  /** The effective profile the run was admitted with (docs/decisions/0026-capability-profiles-and-request-routing.md):
188
195
  * the preset, its machine class and identity, the minutes it ran on and —
189
196
  * when a boundary clipped the budget — the scope that did, so a reader can
@@ -404,6 +411,45 @@ export function routeOfEvents(events: readonly RunEvent[]): RunRouteDecision | u
404
411
  };
405
412
  }
406
413
 
414
+ /** The operator's decision as a record carries it — the `operator` event's
415
+ * fields less the stream bookkeeping (record 0057; run-history item 60). */
416
+ export interface RunOperatorDecision {
417
+ mode: "shadow" | "on";
418
+ outcome: "binds" | "question" | "refusal";
419
+ reason: string;
420
+ binds?: { line: string; reason: string }[];
421
+ question?: string;
422
+ /** A question's proposed line — what the next turn's "yes" binds. */
423
+ proposal?: string;
424
+ refusalCause?: string;
425
+ refusalText?: string;
426
+ intake?: { verdict: string; reason: string };
427
+ latencyMs?: number;
428
+ outputTokens?: number;
429
+ }
430
+
431
+ /** The operator's decision the run's events carry: the one `operator` event
432
+ * (record 0057), whatever the run's own agent source — the shadow decision
433
+ * rides routed and unrouted runs alike, or there would be no disagreement to
434
+ * count. Undefined on a run the operator never saw. */
435
+ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecision | undefined {
436
+ const e = events.find((e) => e.type === "operator");
437
+ if (e?.type !== "operator") return undefined;
438
+ return {
439
+ mode: e.mode,
440
+ outcome: e.outcome,
441
+ reason: e.reason,
442
+ ...(e.binds ? { binds: e.binds.map((b) => ({ line: b.line, reason: b.reason })) } : {}),
443
+ ...(e.question !== undefined ? { question: e.question } : {}),
444
+ ...(e.proposal !== undefined ? { proposal: e.proposal } : {}),
445
+ ...(e.refusalCause !== undefined ? { refusalCause: e.refusalCause } : {}),
446
+ ...(e.refusalText !== undefined ? { refusalText: e.refusalText } : {}),
447
+ ...(e.intake ? { intake: { verdict: e.intake.verdict, reason: e.intake.reason } } : {}),
448
+ ...(e.latencyMs !== undefined ? { latencyMs: e.latencyMs } : {}),
449
+ ...(e.outputTokens !== undefined ? { outputTokens: e.outputTokens } : {}),
450
+ };
451
+ }
452
+
407
453
  function isRunRouteShape(v: unknown): v is RunRouteDecision {
408
454
  if (typeof v !== "object" || v === null) return false;
409
455
  const r = v as Record<string, unknown>;
@@ -23,6 +23,17 @@ export interface ModelUsage {
23
23
  outputTokens: number;
24
24
  cacheReadTokens: number;
25
25
  cacheWriteTokens: number;
26
+ /** The model's dollars summed from its turns' own `usd` attrs (the proxy's
27
+ * meter row, model-proxy item 6): a number when every turn that counted
28
+ * tokens carried one, null when such a turn lacked it (unpriced — a sum
29
+ * that left counted tokens out would understate the model; a turn that
30
+ * counted none, an errored call or a retry, leaves nothing out), absent on
31
+ * a record whose turns predate the meter row (the price table then prices
32
+ * the tokens, costs.md item 4b). */
33
+ usd?: number | null;
34
+ /** The distinct `priceSource` words the turns carried, sorted — what the
35
+ * by-model view names as the source. Absent when no turn carried one. */
36
+ priceSources?: string[];
26
37
  }
27
38
 
28
39
  export interface RunUsage {
@@ -46,6 +57,14 @@ export const emptyUsage = (): RunUsage => ({ turns: 0, byModel: {} });
46
57
  /** The run's usage from its events: one `model.turn` span end per provider call. */
47
58
  export function usageOfEvents(events: readonly RunEvent[]): RunUsage {
48
59
  const usage = emptyUsage();
60
+ // The meter row per model (model-proxy item 6): the turns' own dollars and
61
+ // sources. `usd` sums only when every turn that counted tokens carried one —
62
+ // a turn that counted none (an upstream error, a retry the harness's SDK
63
+ // spent, a stream broken before its usage) has nothing a sum could leave
64
+ // out, so it never turns the model unpriced; a model none of whose turns
65
+ // carried price attrs stays without the fields (a record from before the
66
+ // meter row — the table prices it, costs.md item 4b).
67
+ const priced = new Map<string, { usd: number; pricedTurns: number; tokenTurns: number; sources: Set<string> }>();
49
68
  for (const e of events) {
50
69
  if (e.type !== "span_end" || e.name !== MODEL_TURN) continue;
51
70
  const attrs = (e.attrs ?? {}) as Record<string, unknown>;
@@ -57,6 +76,8 @@ export function usageOfEvents(events: readonly RunEvent[]): RunUsage {
57
76
  cacheReadTokens: 0,
58
77
  cacheWriteTokens: 0,
59
78
  };
79
+ const counted =
80
+ num(attrs.inputTokens) + num(attrs.outputTokens) + num(attrs.cacheReadTokens) + num(attrs.cacheWriteTokens);
60
81
  m.turns += 1;
61
82
  m.inputTokens += num(attrs.inputTokens);
62
83
  m.outputTokens += num(attrs.outputTokens);
@@ -64,38 +85,71 @@ export function usageOfEvents(events: readonly RunEvent[]): RunUsage {
64
85
  m.cacheWriteTokens += num(attrs.cacheWriteTokens);
65
86
  usage.byModel[model] = m;
66
87
  usage.turns += 1;
88
+ const hasUsd = typeof attrs.usd === "number" && Number.isFinite(attrs.usd);
89
+ const hasSource = typeof attrs.priceSource === "string" && attrs.priceSource !== "";
90
+ const p = priced.get(model) ?? { usd: 0, pricedTurns: 0, tokenTurns: 0, sources: new Set<string>() };
91
+ if (counted > 0) p.tokenTurns += 1;
92
+ if (hasUsd) {
93
+ p.usd += attrs.usd as number;
94
+ p.pricedTurns += 1;
95
+ }
96
+ if (hasSource) p.sources.add(attrs.priceSource as string);
97
+ priced.set(model, p);
98
+ }
99
+ for (const [model, p] of priced) {
100
+ if (p.pricedTurns === 0 && p.sources.size === 0) continue;
101
+ const m = usage.byModel[model]!;
102
+ m.usd = p.pricedTurns >= p.tokenTurns ? p.usd : null;
103
+ if (p.sources.size > 0) m.priceSources = [...p.sources].sort();
67
104
  }
68
105
  return usage;
69
106
  }
70
107
 
108
+ /** The two sides' `usd` folded: a sum when both carry one, null when either
109
+ * has an unpriced turn, absent when either predates the meter row (the whole
110
+ * cell then prices from the table, never half a figure). */
111
+ const foldUsd = (a: ModelUsage["usd"], b: ModelUsage["usd"]): ModelUsage["usd"] =>
112
+ a === undefined || b === undefined ? undefined : a === null || b === null ? null : a + b;
113
+
71
114
  export function addUsage(a: RunUsage, b: RunUsage): RunUsage {
72
115
  const out: RunUsage = { turns: a.turns + b.turns, byModel: {} };
73
116
  for (const src of [a.byModel, b.byModel]) {
74
117
  for (const [model, m] of Object.entries(src)) {
75
- const acc = out.byModel[model] ?? {
76
- turns: 0,
77
- inputTokens: 0,
78
- outputTokens: 0,
79
- cacheReadTokens: 0,
80
- cacheWriteTokens: 0,
81
- };
118
+ const acc = out.byModel[model];
119
+ if (!acc) {
120
+ out.byModel[model] = { ...m, ...(m.priceSources ? { priceSources: [...m.priceSources] } : {}) };
121
+ continue;
122
+ }
82
123
  acc.turns += m.turns;
83
124
  acc.inputTokens += m.inputTokens;
84
125
  acc.outputTokens += m.outputTokens;
85
126
  acc.cacheReadTokens += m.cacheReadTokens;
86
127
  acc.cacheWriteTokens += m.cacheWriteTokens;
87
- out.byModel[model] = acc;
128
+ const usd = foldUsd(acc.usd, m.usd);
129
+ if (usd === undefined) delete acc.usd;
130
+ else acc.usd = usd;
131
+ const sources = new Set([...(acc.priceSources ?? []), ...(m.priceSources ?? [])]);
132
+ if (sources.size > 0) acc.priceSources = [...sources].sort();
88
133
  }
89
134
  }
90
135
  return out;
91
136
  }
92
137
 
93
- const isModelUsage = (v: unknown): v is ModelUsage =>
94
- typeof v === "object" &&
95
- v !== null &&
96
- (["turns", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"] as const).every(
97
- (k) => typeof (v as Record<string, unknown>)[k] === "number",
138
+ const isModelUsage = (v: unknown): v is ModelUsage => {
139
+ if (typeof v !== "object" || v === null) return false;
140
+ const m = v as Record<string, unknown>;
141
+ if (
142
+ !["turns", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"].every(
143
+ (k) => typeof m[k] === "number",
144
+ )
145
+ )
146
+ return false;
147
+ if (m.usd !== undefined && m.usd !== null && typeof m.usd !== "number") return false;
148
+ return (
149
+ m.priceSources === undefined ||
150
+ (Array.isArray(m.priceSources) && m.priceSources.every((s) => typeof s === "string"))
98
151
  );
152
+ };
99
153
 
100
154
  export function isRunUsage(v: unknown): v is RunUsage {
101
155
  if (typeof v !== "object" || v === null) return false;
@@ -379,6 +379,9 @@ export interface WatchdogSummary {
379
379
  action?: unknown;
380
380
  error?: string;
381
381
  disk?: unknown;
382
+ /** The resident's last memory reading (`{usedBytes, capBytes, percent, …}`,
383
+ * resident-repos.md item 70) when it has one. */
384
+ memory?: unknown;
382
385
  /** What the pass did about the resident's refresh Workflow instance: `{id, action, why}`. */
383
386
  instance?: unknown;
384
387
  }>;
@@ -439,28 +439,48 @@ export const TIMEOUT_ON_LONG_COMMANDS =
439
439
  "State a timeout on any command you expect to run longer than a minute: a timeout that reaches past the loop's " +
440
440
  "end is refused before the command runs, never cut midway.";
441
441
 
442
- /** The fast gates a plan child runs before every push, named one by one
443
- * (agent-ship item 13; agent-coding item 9). "Its cheapest proving checks"
444
- * left the choice to the child, and children chose wrong: prettier was
445
- * reported clean while `format:check` was red, and hygiene imprints reached
446
- * CI that `hygiene:check` would have caught locally. Naming the commands
447
- * makes each gate a receipt — the exit line goes into the handoff's verified
448
- * list, and a gate the child could not run is unproven, never claimed clean. */
442
+ /** The fast gates a plan child runs before every push, named one by one and
443
+ * each scoped to the changed set (agent-ship item 13; agent-coding item 9).
444
+ * "Its cheapest proving checks" left the choice to the child, and children
445
+ * chose wrong in both directions: prettier was reported clean while
446
+ * `format:check` was red, hygiene imprints reached CI that `hygiene:check`
447
+ * would have caught locally — and children ran the whole suite and the whole
448
+ * typecheck on the shared resident, minutes each call, time-sliced against
449
+ * every other run. So the gates are the changed-set forms, the full runs are
450
+ * said to be CI's alone in the same breath, and each gate is a receipt — the
451
+ * exit line goes into the PR description's validation table as the row's proof
452
+ * (the handoff has no verified list; parseHandoff carries deviations, followUps,
453
+ * unproven and landed), and a gate the child could not run goes under the
454
+ * handoff's unproven list, never claimed clean. The test gate is the touched
455
+ * files by name, never a changed-set or directory run. */
456
+ /** The test command the contract hands a child: the touched files by name, once.
457
+ * Never `--changed`: against a base that moves, it selects most of the suite, and on the
458
+ * shared resident that is the memory incident the coding contract exists to prevent. */
459
+ export const TOUCHED_TESTS_COMMAND = "`npx vitest run` on the test files you touched, by name,";
460
+
449
461
  export const FAST_GATES_BEFORE_PUSH =
450
- "The fast gates, before every push: `npx prettier --check` on the changed files, `npm run hygiene:check`, " +
451
- "`npm run specs:check`, and `tsc --noEmit` on the touched project under `NODE_OPTIONS=--max-old-space-size=6144`. " +
452
- "Paste each command's exit line into the handoff's verified list; a gate you could not run goes under unproven " +
453
- "and is never claimed clean.";
462
+ "The fast gates, before every push — each scoped to the changed set, never the whole project: " +
463
+ `${TOUCHED_TESTS_COMMAND} once (never \`--changed\`, never a directory: on a moving base that is most of the suite), ` +
464
+ "`tsc --noEmit -p` the touched tsconfig under `NODE_OPTIONS=--max-old-space-size=6144`, " +
465
+ "`npx prettier --check` on the changed files, `npm run hygiene:check` and `npm run specs:check` — " +
466
+ "then your judgement on what else this change needs, not a longer checklist. Every CI pipeline runs the " +
467
+ "tests, the types, the formatting and the full verification on your push, so you never run them again: " +
468
+ "you validate and fix your own change before pushing, at the changed-set scope. Passing the full test suite " +
469
+ "and the full typecheck is NOT part of your criteria: CI is that gate and the only place they run — on a " +
470
+ "shared resident they cost minutes that every other run pays for. " +
471
+ "Paste each command's exit line into the PR description's validation table as the row's proof; a gate you " +
472
+ "could not run goes under the handoff's unproven list and is never claimed clean.";
454
473
 
455
474
  function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
456
475
  const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
457
476
  const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
477
+ const gates = FAST_GATES_BEFORE_PUSH;
458
478
  return (
459
479
  `Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
460
480
  `the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
461
- `Push the branch as soon as the change exists and the fast gates pass — before the project's ` +
462
- `full verification, which runs after that push with any fix as a further commit; an unpushed tree does not ` +
463
- `survive the run's end. ${FAST_GATES_BEFORE_PUSH} Right before each push, fetch ${onto} again and rebase ` +
481
+ `Push the branch as soon as the change exists and the fast gates pass — the project's full verification ` +
482
+ `is CI's gate, run there after the push with any fix as a further commit; an unpushed tree does not ` +
483
+ `survive the run's end. ${gates} Right before each push, fetch ${onto} again and rebase ` +
464
484
  `once more if it moved while you worked, so the pull request is not born conflicting. At the wind-down note, ` +
465
485
  `commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`
466
486
  );