@coreplane/switchboard 1.252.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 (131) hide show
  1. package/dist/assets/config/config.example.yaml +24 -0
  2. package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
  3. package/dist/assets/deploy/cloudflare-memory/worker.ts +146 -26
  4. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
  5. package/dist/assets/deploy/profile.example.json +1 -1
  6. package/dist/assets/package-lock.json +3 -3
  7. package/dist/assets/package.json +1 -1
  8. package/dist/assets/source.json +3 -3
  9. package/dist/assets/src/core/authz/policy.ts +8 -0
  10. package/dist/assets/src/core/budgets.ts +17 -0
  11. package/dist/assets/src/core/coordinator/contract.ts +51 -1
  12. package/dist/assets/src/core/coordinator/driver.ts +102 -11
  13. package/dist/assets/src/core/memory/engine.ts +25 -4
  14. package/dist/assets/src/core/memory/types.ts +6 -2
  15. package/dist/assets/src/core/runEvents.ts +31 -1
  16. package/dist/assets/src/core/runFriction.ts +1 -0
  17. package/dist/assets/src/core/runMetrics.ts +237 -0
  18. package/dist/assets/src/core/runRecord.ts +46 -0
  19. package/dist/assets/src/core/ship/contract.ts +8 -15
  20. package/dist/assets/src/core/ship/coordinator.ts +105 -2
  21. package/dist/assets/src/deploy/profile.ts +16 -0
  22. package/dist/assets/web/dist/.vite/manifest.json +440 -426
  23. package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
  24. package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
  25. package/dist/assets/web/dist/assets/{DeliveryPage-3ELQWM0r.js → DeliveryPage-C7jaEJip.js} +1 -1
  26. package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
  27. package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
  28. package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
  29. package/dist/assets/web/dist/assets/{PendingTurnRow-ChCQOLgZ.js → PendingTurnRow-C-pUgyyX.js} +1 -1
  30. package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
  31. package/dist/assets/web/dist/assets/{ResidentDetailPage-C9y3nbo8.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
  32. package/dist/assets/web/dist/assets/{ResidentsIndexPage-i1RG9e7g.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
  33. package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
  34. package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
  35. package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
  36. package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
  37. package/dist/assets/web/dist/assets/{ScheduledPage-DvYwM2TE.js → ScheduledPage-C8FYoZy5.js} +1 -1
  38. package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
  39. package/dist/assets/web/dist/assets/{SettingsPage-Bo6yCyXZ.js → SettingsPage-CkYIhLUl.js} +1 -1
  40. package/dist/assets/web/dist/assets/{StatusDot-CAfS1AUi.js → StatusDot-C0TUgnC1.js} +1 -1
  41. package/dist/assets/web/dist/assets/{Tooltip-tZoum_T-.js → Tooltip-CN5cZdl3.js} +1 -1
  42. package/dist/assets/web/dist/assets/{UnitRoutePage-BmdOHwNn.js → UnitRoutePage-D_EtAl8r.js} +1 -1
  43. package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
  44. package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
  45. package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
  46. package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
  47. package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
  48. package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
  49. package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
  50. package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
  51. package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
  52. package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
  53. package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
  54. package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
  55. package/dist/assets/web/dist/assets/{dist-DfbEpHXR.js → dist-B1Mf2JbM.js} +2 -2
  56. package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
  57. package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
  58. package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
  59. package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
  60. package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
  61. package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
  62. package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
  63. package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
  64. package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
  65. package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
  66. package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
  67. package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
  68. package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
  69. package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
  70. package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
  71. package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
  72. package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
  73. package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
  74. package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
  75. package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
  76. package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
  77. package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
  78. package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
  79. package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
  80. package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
  81. package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
  82. package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
  83. package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
  84. package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
  85. package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
  86. package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
  87. package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
  88. package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
  89. package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
  90. package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
  91. package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
  92. package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
  93. package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
  94. package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
  95. package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
  96. package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
  97. package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
  98. package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
  99. package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
  100. package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
  101. package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
  102. package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
  103. package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
  104. package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
  105. package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
  106. package/dist/assets/web/dist/assets/{sseReplay-DmyMXfRC.js → sseReplay-BYo2EZKy.js} +4 -4
  107. package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
  108. package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
  109. package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
  110. package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
  111. package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
  112. package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
  113. package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
  114. package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
  115. package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
  116. package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
  117. package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
  118. package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
  119. package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
  120. package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
  121. package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
  122. package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
  123. package/dist/cli.js +19192 -17592
  124. package/package.json +1 -1
  125. package/dist/assets/web/dist/assets/HomePage-BG_ok-K2.js +0 -2
  126. package/dist/assets/web/dist/assets/RunFoldRow-D3wVpzBa.js +0 -1
  127. package/dist/assets/web/dist/assets/RunRoutePage-B3IirUVi.js +0 -9
  128. package/dist/assets/web/dist/assets/RunsIndexPage-DiFmtGaJ.js +0 -1
  129. package/dist/assets/web/dist/assets/indexRow-BT0cPVRw.js +0 -1
  130. package/dist/assets/web/dist/assets/main-5Gm_1Gv8.js +0 -28
  131. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
@@ -8,9 +8,10 @@
8
8
  // parent record the spawn route reads the requester from, and the names the
9
9
  // routes decide on.
10
10
  //
11
- import type { Grant, GrantSource } from "../budgets.js";
11
+ import { IDLE_DAYS_MAX, type Grant, type GrantSource } from "../budgets.js";
12
12
  import { isVerbosity, type Verbosity } from "../verbosity.js";
13
13
  import { isAddressSeverity, type AddressSeverity, type AddressSeveritySource } from "../ship/coordinator.js";
14
+ import { isHandoffShape, type Handoff } from "../ship/handoff.js";
14
15
 
15
16
  // A coordinator is a Workflow instance in the shim Worker whose children are
16
17
  // ordinary `dispatch()` runs as the requesting user. It holds no credential of
@@ -329,6 +330,11 @@ export interface CoordinatorInstance {
329
330
  * the unit threads it owns — the unit-ending report's asides and the
330
331
  * segment lines are `verbose` material. Absent reads as `quiet`. */
331
332
  verbosity?: Verbosity;
333
+ /** The idle flag (record 0051; agent-ship item 8): `ship.idleDays` as the
334
+ * ship fork resolved it (user > channel > org), written here beside the
335
+ * grant and answered by the plan route — above zero, an idling ending
336
+ * becomes `idle`; absent reads as zero, today's endings. */
337
+ idleDays?: number;
332
338
  /** The pipeline's caps as the profile gate clipped them: the rounds cap and the wall clock per unit. */
333
339
  caps?: { maxRounds: number; maxMinutes: number };
334
340
  /** The status card in the requesting thread, when the channel has one — what
@@ -350,6 +356,24 @@ export interface UnitSegment {
350
356
  at: number;
351
357
  }
352
358
 
359
+ /** The longest `why` an idle carries: an ending kind's name, never prose —
360
+ * the route refuses a longer one at the door, the row validator at the store. */
361
+ export const IDLE_WHY_MAX = 64;
362
+
363
+ /** The idle on a unit's row (record 0051): what a continuation needs, and the
364
+ * wakes spent against `IDLE_WAKES_MAX`. */
365
+ export interface UnitIdle {
366
+ /** The old ending kind the idle stands in for (`wall_clock_cap`, `stopped`, `continued`, …). */
367
+ why: string;
368
+ at: number;
369
+ renewalsLeft: number;
370
+ from?: string;
371
+ runId?: string;
372
+ spendUsd: number | null;
373
+ handoff?: Handoff;
374
+ wakes: number;
375
+ }
376
+
353
377
  /** One unit of the plan an instance runs (a task string is a generated plan of
354
378
  * one unit, `U1`): its branch, the units it waits on, and — as the runner
355
379
  * reaches it — its thread, its pull request, the round boundaries the card
@@ -398,6 +422,15 @@ export interface CoordinatorUnit {
398
422
  * renews the same segment twice. `from` is the sha the segment continues
399
423
  * from, `runId` the coding run whose write-up briefs it. */
400
424
  segments?: UnitSegment[];
425
+ /** The unit idles (record 0051; run-history item 50): written by `unit-end`
426
+ * on an `idle` ending in place of `ending`, so the unit stays unfinished
427
+ * and keeps owning its thread. `why` is the old kind, `renewalsLeft` what
428
+ * the grant still holds (the wake spends one — this plan's fifth unit),
429
+ * `from` the head a continuation opens from, `runId` the last coding
430
+ * child's run (absent when none ran), `spendUsd` the session's dollars
431
+ * (null once any run's cost is unknown), `handoff` that child's lists, and
432
+ * `wakes` how many wakes this idle has answered — zero at the write. */
433
+ idle?: UnitIdle;
401
434
  /** The round boundaries the coordinator reported, oldest first (the `ship_round`
402
435
  * vocabulary). `gate` rides an approve the machine's severity check caught
403
436
  * carrying a finding at or above the level in force ([agent-ship](../../../docs/reference/specs/agent-ship.md)
@@ -450,6 +483,11 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
450
483
  if (r.caps !== undefined && !(isObject(r.caps) && isFinite(r.caps.maxRounds) && isFinite(r.caps.maxMinutes)))
451
484
  return false;
452
485
  if (r.card !== undefined && !(isObject(r.card) && isText(r.card.channel) && isText(r.card.ts))) return false;
486
+ if (
487
+ r.idleDays !== undefined &&
488
+ !(Number.isInteger(r.idleDays) && (r.idleDays as number) >= 0 && (r.idleDays as number) <= IDLE_DAYS_MAX)
489
+ )
490
+ return false;
453
491
  if (!isOptionalText(r.runId) || !isOptionalText(r.label)) return false;
454
492
  if (r.verbosity !== undefined && !isVerbosity(r.verbosity)) return false;
455
493
  if (r.attempt !== undefined && !(Number.isInteger(r.attempt) && (r.attempt as number) >= 2)) return false;
@@ -464,6 +502,17 @@ const isRoundGate = (v: unknown): boolean =>
464
502
  Array.isArray(v.findings) &&
465
503
  v.findings.every((f) => typeof f === "string");
466
504
 
505
+ const isUnitIdle = (v: unknown): boolean =>
506
+ isObject(v) &&
507
+ isText(v.why, IDLE_WHY_MAX) &&
508
+ isFinite(v.at) &&
509
+ isCount(v.renewalsLeft) &&
510
+ (v.from === undefined || isText(v.from)) &&
511
+ (v.runId === undefined || isText(v.runId)) &&
512
+ (v.spendUsd === null || isFinite(v.spendUsd)) &&
513
+ (v.handoff === undefined || isHandoffShape(v.handoff)) &&
514
+ isCount(v.wakes);
515
+
467
516
  const isSegment = (v: unknown): boolean =>
468
517
  isObject(v) &&
469
518
  typeof v.index === "number" &&
@@ -486,6 +535,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
486
535
  if (r.resume !== undefined && !isResume(r.resume)) return false;
487
536
  if (r.lastPush !== undefined && !isText(r.lastPush)) return false;
488
537
  if (r.segments !== undefined && (!Array.isArray(r.segments) || !r.segments.every(isSegment))) return false;
538
+ if (r.idle !== undefined && !isUnitIdle(r.idle)) return false;
489
539
  if (
490
540
  !Array.isArray(r.rounds) ||
491
541
  r.rounds.length > MAX_ROUNDS ||
@@ -33,7 +33,7 @@
33
33
  // plan's re-issue. A task string's ship branch waits for a person. Node-free:
34
34
  // the shim Worker imports this by relative path.
35
35
 
36
- import { DEFAULT_GRANT, GRANT_RENEWALS_MAX, type Grant, type GrantSource } from "../budgets.js";
36
+ import { DEFAULT_GRANT, GRANT_RENEWALS_MAX, IDLE_DAYS_MAX, type Grant, type GrantSource } from "../budgets.js";
37
37
  import { DEFAULT_VERBOSITY, isVerbosity, type Verbosity } from "../verbosity.js";
38
38
  import {
39
39
  applyReturn,
@@ -42,6 +42,8 @@ import {
42
42
  openPlanCursor,
43
43
  openUnitPipeline,
44
44
  readyUnits,
45
+ type PlanCursor,
46
+ type UnitStatus,
45
47
  renderUnitReport,
46
48
  settleUnit,
47
49
  startUnit,
@@ -163,8 +165,18 @@ export function readBotAnswer(
163
165
  }
164
166
 
165
167
  /** The answers the bot itself calls a passing condition — the step is asked
166
- * again, under the policy. Everything else, refusals included, is the machine's. */
167
- const TRANSIENT = new Set(["github_unavailable", "no_channel", "thread_failed", "unit_not_started", "not_host"]);
168
+ * again, under the policy. Everything else, refusals included, is the machine's.
169
+ * `queued` is the plane's hold on a spawn (record 0064, "The queue"): a `409`
170
+ * saying the child's admission waits on an event — the step re-asks until the
171
+ * child is admitted, and the instance is never failed over a full plane. */
172
+ const TRANSIENT = new Set([
173
+ "github_unavailable",
174
+ "no_channel",
175
+ "thread_failed",
176
+ "unit_not_started",
177
+ "not_host",
178
+ "queued",
179
+ ]);
168
180
  export function transientRefusal(answer: BotAnswer): string | undefined {
169
181
  const { ok, error, message } = answer.body;
170
182
  if (ok !== false || typeof error !== "string" || !TRANSIENT.has(error)) return undefined;
@@ -191,6 +203,8 @@ interface PlanFacts {
191
203
  grantSource: GrantSource;
192
204
  /** The request's verbosity as the plan route answers it (routing-and-config item 28): what the unit threads hear. */
193
205
  verbosity: Verbosity;
206
+ /** The idle flag beside the grant (record 0051): above zero, an idling ending becomes `idle`. */
207
+ idleDays: number;
194
208
  /** The instance's mark as the plan route answers it: a generated one-unit plan (a `plan` with no `path`). */
195
209
  generated: boolean;
196
210
  /** The runs page base the bot answered: the report links a child's write-up to its run page with it. */
@@ -220,6 +234,12 @@ function readGrant(raw: unknown): Grant {
220
234
  return { renewals: raw.renewals, ...(typeof cap === "number" && cap > 0 ? { costCapUsd: cap } : {}) };
221
235
  }
222
236
 
237
+ /** The idle flag as the plan route answers it (record 0051): an integer from 0
238
+ * to the module's cap; anything unreadable is 0 — nothing idles on a guess. */
239
+ function readIdleDays(raw: unknown): number {
240
+ return typeof raw === "number" && Number.isInteger(raw) && raw >= 0 && raw <= IDLE_DAYS_MAX ? raw : 0;
241
+ }
242
+
223
243
  function readPlan(a: BotAnswer): PlanFacts {
224
244
  const b = a.body;
225
245
  if (b.ok !== true) throw new UnreadableAnswer("plan", a, "not ok");
@@ -240,6 +260,7 @@ function readPlan(a: BotAnswer): PlanFacts {
240
260
  grantSource:
241
261
  b.grantSource === "run" || b.grantSource === "user" || b.grantSource === "channel" ? b.grantSource : "org",
242
262
  verbosity: isVerbosity(b.verbosity) ? b.verbosity : DEFAULT_VERBOSITY,
263
+ idleDays: readIdleDays(b.idleDays),
243
264
  generated: b.generated === true,
244
265
  ...(typeof b.runPageBase === "string" && b.runPageBase.length > 0 ? { runPageBase: b.runPageBase } : {}),
245
266
  repo: b.repo,
@@ -662,6 +683,7 @@ async function runUnit(
662
683
  grant: plan.grant,
663
684
  grantSource: plan.grantSource,
664
685
  verbosity: plan.verbosity,
686
+ idleDays: plan.idleDays,
665
687
  generated: plan.generated,
666
688
  ...(plan.runPageBase !== undefined ? { runPageBase: plan.runPageBase } : {}),
667
689
  ...(resume !== undefined ? { resume } : {}),
@@ -734,13 +756,28 @@ async function runUnit(
734
756
  kind: note.ending.kind,
735
757
  report: renderUnitReport(state, endFacts),
736
758
  threadReport: renderUnitReport(state, endFacts, state.input.verbosity ?? DEFAULT_VERBOSITY),
759
+ // An idle ending carries its continuation facts (record 0051): the
760
+ // bot writes them on the row's `idle` in place of an ending, with
761
+ // the coding run id it already receives below.
762
+ ...(note.ending.kind === "idle"
763
+ ? {
764
+ why: note.ending.why,
765
+ renewalsLeft: note.ending.renewalsLeft,
766
+ ...(note.ending.from !== undefined ? { from: note.ending.from } : {}),
767
+ spendUsd: note.ending.spendUsd,
768
+ ...(note.ending.handoff !== undefined ? { handoff: note.ending.handoff } : {}),
769
+ }
770
+ : {}),
737
771
  },
738
772
  ...(state.pr !== undefined ? { pr: state.pr } : {}),
739
773
  // A review_pending ending names the child's own last push so the next
740
- // attempt's pre-check can start at the review round (the row's lastPush).
774
+ // attempt's pre-check can start at the review round (the row's lastPush)
775
+ // — an idled one the same, off the idle's `from` (record 0051).
741
776
  ...(note.ending.kind === "review_pending" && note.ending.headSha !== undefined
742
777
  ? { headSha: note.ending.headSha }
743
- : {}),
778
+ : note.ending.kind === "idle" && note.ending.why === "review_pending" && note.ending.from !== undefined
779
+ ? { headSha: note.ending.from }
780
+ : {}),
744
781
  ...(state.lastCodingRunId !== undefined ? { codingRunId: state.lastCodingRunId } : {}),
745
782
  // A continued ending is a segment's end, not the unit's: the bot
746
783
  // writes the renewal as a row keyed by the next segment's index
@@ -768,12 +805,9 @@ function blockedReport(unit: string, dep: string, depEnding: string): string {
768
805
  return `⛔ Blocked: ${unit} waits on ${dep}, which ended ${depEnding}${person ? " — a person's merge" : ""}. Re-issue the plan naming the remaining units once it is ${person ? "merged" : "resolved"}.`;
769
806
  }
770
807
 
771
- /** The plan: its units one at a time in dependency order, then the endings of the units it never reached. */
772
- async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string): Promise<PlanRunSummary> {
773
- const plan = readPlan(
774
- answerOf("plan", await step.do("plan", STEP_CONFIG, () => call(bot, "plan", { parentInstanceId: instanceId }))),
775
- );
776
- const graph: PlanGraph = {
808
+ /** The graph as one plan answer carries it: the instance's unit rows, in the plan's order. */
809
+ function graphOf(plan: PlanFacts, instanceId: string): PlanGraph {
810
+ return {
777
811
  planId: plan.planId ?? instanceId,
778
812
  units: plan.units.map((u) => ({
779
813
  id: u.unit,
@@ -783,6 +817,54 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
783
817
  dependsOn: u.dependsOn,
784
818
  })),
785
819
  };
820
+ }
821
+
822
+ /** The cursor over a freshly read selection (the orchestration-plane plan's
823
+ * re-read requirement): a surviving unit keeps its standing, a row appended
824
+ * mid-walk joins pending — walked after the current unit — and a row gone from the selection
825
+ * was merged by hand: it is walked as merged with nothing run and nothing
826
+ * told (`gone`), a dependency on it counted satisfied like any dependency
827
+ * outside the selection. `blocked` is derived state, so it is recomputed over
828
+ * the fresh graph rather than carried — a blocked unit whose failing
829
+ * dependency left the selection is in play again. */
830
+ function rereadCursor(cursor: PlanCursor, fresh: PlanGraph): { cursor: PlanCursor; gone: string[] } {
831
+ const gone = cursor.order.filter((id) => !fresh.units.some((u) => u.id === id));
832
+ const order = fresh.units.map((u) => u.id);
833
+ const status: Record<string, UnitStatus> = {};
834
+ for (const id of order) {
835
+ const prior = cursor.status[id];
836
+ status[id] = prior === undefined || prior === "blocked" ? "pending" : prior;
837
+ }
838
+ let changed = true;
839
+ while (changed) {
840
+ changed = false;
841
+ for (const u of fresh.units) {
842
+ if (status[u.id] !== "pending") continue;
843
+ if (u.dependsOn.some((d) => d in status && (status[d] === "failed" || status[d] === "blocked"))) {
844
+ status[u.id] = "blocked";
845
+ changed = true;
846
+ }
847
+ }
848
+ }
849
+ return { cursor: { order, status }, gone };
850
+ }
851
+
852
+ /** The plan: its units one at a time in dependency order, then the endings of the units it never reached. */
853
+ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string): Promise<PlanRunSummary> {
854
+ // The selection is read at every unit boundary (the orchestration-plane plan): the first read opens
855
+ // the cursor, and each boundary's — its own durable step, `plan/<n>`, so a
856
+ // replay meets the same read — rebuilds it, so a later bot can append a unit
857
+ // to a live instance or drop one merged by hand without killing it.
858
+ let reads = 0;
859
+ const readSelection = async (): Promise<PlanFacts> => {
860
+ reads += 1;
861
+ const name = reads === 1 ? "plan" : `plan/${reads}`;
862
+ return readPlan(
863
+ answerOf("plan", await step.do(name, STEP_CONFIG, () => call(bot, "plan", { parentInstanceId: instanceId }))),
864
+ );
865
+ };
866
+ let plan = await readSelection();
867
+ let graph = graphOf(plan, instanceId);
786
868
  let cursor = openPlanCursor(graph);
787
869
  const endings: Record<string, string> = {};
788
870
  for (;;) {
@@ -809,7 +891,16 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
809
891
  endings[next] = ending.kind;
810
892
  // A unit is done for its dependents when the base carries its scope: the
811
893
  // runner's merge, or a scope that had already landed before the attempt.
894
+ // An `idle` ending settles the unit `failed` for now: nothing waits yet —
895
+ // the indexed wait and the wake land with the fifth unit of record 0051's
896
+ // plan — so the walk is unchanged until then and the flag ships at zero.
812
897
  cursor = settleUnit(graph, cursor, next, isSettledDone(ending.kind) ? "done" : "failed");
898
+ // The unit boundary's re-read (the orchestration-plane plan): the fresh rows are the selection now.
899
+ plan = await readSelection();
900
+ graph = graphOf(plan, instanceId);
901
+ const reread = rereadCursor(cursor, graph);
902
+ cursor = reread.cursor;
903
+ for (const id of reread.gone) endings[id] ??= "merged";
813
904
  }
814
905
  // Blocked units, in the plan's order: each told its own ending, so the rows
815
906
  // and the summary say why it never ran. Every blocked unit's ending is known
@@ -59,15 +59,25 @@ export function planEviction(active: MemoryRecord[], cap: number): MemoryRecord[
59
59
  .slice(0, excess);
60
60
  }
61
61
 
62
- /** What a store must do for one candidate. `dedup`: bump `target.useCount`,
63
- * insert nothing. `insert`: append `record` (already minted) and, when
64
- * `supersede` is set, flip that record to `superseded`. */
62
+ /** What a store must do for one candidate. `restate`: bump `target.useCount`,
63
+ * set its `lastUsedAt` to the store's now and its confidence to `confidence`
64
+ * when the plan carries one (the higher of the two sides), insert nothing.
65
+ * `dedup`: bump `target.useCount`, insert nothing. `insert`: append `record`
66
+ * (already minted) and, when `supersede` is set, flip that record to
67
+ * `superseded`. */
65
68
  export type WritePlan =
66
- { action: "dedup"; target: MemoryRecord } | { action: "insert"; record: MemoryRecord; supersede?: MemoryRecord };
69
+ | { action: "restate"; target: MemoryRecord; confidence?: number }
70
+ | { action: "dedup"; target: MemoryRecord }
71
+ | { action: "insert"; record: MemoryRecord; supersede?: MemoryRecord };
67
72
 
68
73
  /**
69
74
  * Decide how one candidate lands among a scope's ACTIVE records (docs/reference/specs/
70
75
  * memory.md §8):
76
+ * - **Restate target** = the active same-scope record whose id equals
77
+ * `cand.restates`: the fact re-teaches a shown record, so that record is
78
+ * refreshed — usage bumped, confidence the higher of the two — and nothing
79
+ * is inserted, whatever the candidate's wording. A missing or non-active
80
+ * target falls through to the rules below (today's dedup-or-insert).
71
81
  * - **Supersede target** = the active same-scope record whose id equals
72
82
  * `cand.supersedes`; an unknown/foreign id resolves to nothing (the new
73
83
  * record still lands, superseding nothing).
@@ -86,6 +96,17 @@ export function planWrite(
86
96
  cand: MemoryCandidate,
87
97
  mint: (cand: MemoryCandidate) => MemoryRecord,
88
98
  ): WritePlan {
99
+ if (cand.restates) {
100
+ const restated = active.find((r) => r.status === "active" && r.id === cand.restates);
101
+ if (restated) {
102
+ const confidences = [restated.confidence, cand.confidence].filter((c): c is number => c !== undefined);
103
+ return {
104
+ action: "restate",
105
+ target: restated,
106
+ ...(confidences.length > 0 ? { confidence: Math.max(...confidences) } : {}),
107
+ };
108
+ }
109
+ }
89
110
  const norm = normalizeText(cand.text);
90
111
  const target = cand.supersedes ? active.find((r) => r.status === "active" && r.id === cand.supersedes) : undefined;
91
112
  const dedupPool = cand.supersedes ? (target ? [target] : []) : active.filter((r) => r.status === "active");
@@ -54,12 +54,16 @@ export interface MemoryCandidate {
54
54
  sourceRunId?: string;
55
55
  confidence?: number;
56
56
  supersedes?: string;
57
+ /** id of a shown record this fact restates: the store bumps that record
58
+ * (`useCount`, `lastUsedAt`, the higher `confidence`) instead of inserting
59
+ * a twin; a target that is missing or not active falls through to the
60
+ * plain dedup-or-insert path. */
61
+ restates?: string;
57
62
  }
58
63
 
59
64
  /** What one `write` batch actually did, per candidate action — the seam's
60
65
  * receipt (the counters on the `[memory]` outcome line are these plus the
61
- * parse gate's own). `restated` stays 0 until the restate action lands on the
62
- * write plan; it is on the shape now so every store answers the same fields. */
66
+ * parse gate's own). */
63
67
  export interface WriteCounts {
64
68
  /** Candidates minted as new active records. */
65
69
  inserted: number;
@@ -405,7 +405,11 @@ export type ShipRoundOutcome =
405
405
  | "stopped"
406
406
  /** The coding round ended at its lease with the unit unfinished and the row
407
407
  * showing progress, and the grant renewed: the next segment opens (decision 0046). */
408
- | "continued";
408
+ | "continued"
409
+ /** The unit idles instead of ending (record 0051): emitted once the wake
410
+ * lands (that plan's fifth unit); in the vocabulary now so rows written
411
+ * then read beside today's — the old kinds stay for rows already written. */
412
+ | "idle";
409
413
 
410
414
  /**
411
415
  * One event in a run's stream. `seq` is stamped by `RunRegistry.publish` — a
@@ -1003,6 +1007,32 @@ export type RunEvent =
1003
1007
  seq?: number;
1004
1008
  at?: number;
1005
1009
  }
1010
+ /** The operator's decision beside the routed request ([record 0057](../../docs/decisions/0057-the-operator-is-the-one-door-a-model-binds-every-chat-input-and-deterministic-code-authorizes-fences-and-executes.md);
1011
+ * the one-door plan's operator unit; run-history item 60): one per admitted chat
1012
+ * event under `routing.operator: shadow` or `on`, published beside the
1013
+ * `route` event. The decision is binds, a question or a refusal; a bind's
1014
+ * `line` is redacted and cut like the receipt (`ROUTE_RECEIPT_CAP`), never
1015
+ * the message text; `intake` carries the intake gate's verdict when the
1016
+ * gate is present; `latencyMs` and `outputTokens` feed the replay's median
1017
+ * rows. Under `shadow` nothing runs from it. Additive: unknown → ignored. */
1018
+ | {
1019
+ type: "operator";
1020
+ mode: "shadow" | "on";
1021
+ outcome: "binds" | "question" | "refusal";
1022
+ reason: string;
1023
+ binds?: ReadonlyArray<{ line: string; reason: string }>;
1024
+ question?: string;
1025
+ /** A question's proposed line, redacted and cut like the receipt — what
1026
+ * the next turn's "yes" binds (`bindFromAnswer`). */
1027
+ proposal?: string;
1028
+ refusalCause?: string;
1029
+ refusalText?: string;
1030
+ intake?: { verdict: string; reason: string };
1031
+ latencyMs?: number;
1032
+ outputTokens?: number;
1033
+ seq?: number;
1034
+ at?: number;
1035
+ }
1006
1036
  /** A refusal the door made ([record 0054](../../docs/decisions/0054-a-refusal-the-person-caused-is-one-question-with-a-best-guess.md),
1007
1037
  * as amended: every refusal is a run record; run-history.md item 2): the
1008
1038
  * code, its one cause, and the sentence the person read — redacted and
@@ -431,6 +431,7 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
431
431
  ev.type === "ship_round" ||
432
432
  ev.type === "ship_unit" ||
433
433
  ev.type === "route" ||
434
+ ev.type === "operator" ||
434
435
  ev.type === "refusal" ||
435
436
  ev.type === "reference" ||
436
437
  ev.type === "lease" ||
@@ -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
+ }