@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.
- package/dist/assets/config/config.example.yaml +24 -0
- package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +146 -26
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
- package/dist/assets/deploy/profile.example.json +1 -1
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/authz/policy.ts +8 -0
- package/dist/assets/src/core/budgets.ts +17 -0
- package/dist/assets/src/core/coordinator/contract.ts +51 -1
- package/dist/assets/src/core/coordinator/driver.ts +102 -11
- package/dist/assets/src/core/memory/engine.ts +25 -4
- package/dist/assets/src/core/memory/types.ts +6 -2
- package/dist/assets/src/core/runEvents.ts +31 -1
- package/dist/assets/src/core/runFriction.ts +1 -0
- package/dist/assets/src/core/runMetrics.ts +237 -0
- package/dist/assets/src/core/runRecord.ts +46 -0
- package/dist/assets/src/core/ship/contract.ts +8 -15
- package/dist/assets/src/core/ship/coordinator.ts +105 -2
- package/dist/assets/src/deploy/profile.ts +16 -0
- package/dist/assets/web/dist/.vite/manifest.json +440 -426
- package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-3ELQWM0r.js → DeliveryPage-C7jaEJip.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
- package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-ChCQOLgZ.js → PendingTurnRow-C-pUgyyX.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-C9y3nbo8.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-i1RG9e7g.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-DvYwM2TE.js → ScheduledPage-C8FYoZy5.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-Bo6yCyXZ.js → SettingsPage-CkYIhLUl.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-CAfS1AUi.js → StatusDot-C0TUgnC1.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-tZoum_T-.js → Tooltip-CN5cZdl3.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BmdOHwNn.js → UnitRoutePage-D_EtAl8r.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
- package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-DfbEpHXR.js → dist-B1Mf2JbM.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
- package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
- package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
- package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
- package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
- package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
- package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
- package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
- package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
- package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
- package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
- package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DmyMXfRC.js → sseReplay-BYo2EZKy.js} +4 -4
- package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
- package/dist/cli.js +19192 -17592
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-BG_ok-K2.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-D3wVpzBa.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-B3IirUVi.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DiFmtGaJ.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BT0cPVRw.js +0 -1
- package/dist/assets/web/dist/assets/main-5Gm_1Gv8.js +0 -28
- 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
|
|
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
|
-
|
|
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
|
|
772
|
-
|
|
773
|
-
|
|
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. `
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
-
|
|
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).
|
|
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
|
+
}
|