@coreplane/switchboard 1.241.0 → 1.243.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/config/config.example.yaml +45 -13
- package/dist/assets/deploy/cloudflare-memory/worker.ts +86 -29
- package/dist/assets/deploy/cloudflare-resident/worker.ts +298 -24
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +38 -5
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +5 -5
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +61 -48
- package/dist/assets/src/config/profile.ts +68 -3
- package/dist/assets/src/core/authz/actor.ts +12 -9
- package/dist/assets/src/core/authz/authorize.ts +17 -4
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/types.ts +12 -0
- package/dist/assets/src/core/budgets.ts +313 -0
- package/dist/assets/src/core/coordinator/contract.ts +3 -1
- package/dist/assets/src/core/coordinator/driver.ts +0 -10
- package/dist/assets/src/core/costs.ts +21 -80
- package/dist/assets/src/core/harness/scope.ts +21 -0
- package/dist/assets/src/core/modelPricing.ts +212 -0
- package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
- package/dist/assets/src/core/reviewVerdict.ts +7 -0
- package/dist/assets/src/core/runEvents.ts +48 -5
- package/dist/assets/src/core/runFriction.ts +2 -1
- package/dist/assets/src/core/runLedger/types.ts +2 -0
- package/dist/assets/src/core/runRecord.ts +51 -0
- package/dist/assets/src/core/runUsage.ts +158 -47
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +4 -2
- package/dist/assets/src/core/ship/coordinator.ts +101 -84
- package/dist/assets/src/core/ship/handoff.ts +9 -0
- package/dist/assets/src/execution/bashTimeout.ts +8 -5
- package/dist/assets/src/execution/residentAutoRebuild.ts +4 -2
- package/dist/assets/src/execution/residentHead.ts +23 -10
- package/dist/assets/src/execution/residentInfraStreak.ts +89 -0
- package/dist/assets/src/execution/residentRefresh.ts +43 -4
- package/dist/assets/src/execution/residentSteps.ts +1 -0
- package/dist/assets/src/execution/sandboxErrors.ts +6 -0
- package/dist/assets/src/execution/seedPlan.ts +17 -0
- package/dist/assets/web/dist/.vite/manifest.json +443 -446
- package/dist/assets/web/dist/assets/{AppShell-DEy5jRuS.js → AppShell-IDMGG6yi.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +2 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-BGpPr5xr.js → DeliveryPage-n1tz5I_v.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DtmQM2gD.js → NotFoundPage-C97EhJcQ.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-C-48fxat.js → ResidentDetailPage-DACalNVF.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-CtNGNnhN.js → RunsTabs-BUfdk0lH.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-DPWE1JmO.js → StatusDot-Dw0T1M-P.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DKhSRH9t.js → Tooltip-BbLuIAiS.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-DgSK1qvr.js → angular-html-oBNfPJR0.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-D3gNdiSG.js → angular-ts-BvNwsyWA.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CkHCYM8I.js → apl-CNUdRlYf.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DLm45axt.js → astro-Zb0NriSe.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-BKa-VE-c.js → blade-qPRVheqq.js} +1 -1
- package/dist/assets/web/dist/assets/{c-C8NCNWai.js → c-D8Awx4YO.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DqDQ7IKH.js → chapel-Bt72Mhsx.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-j4vCD6AJ.js → cobol-BOBacexg.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-C6oDVH-0.js → coffee-E4u0liHW.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-Bd3A1Rtc.js → cpp-q2sLNlul.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CUOgOw0d.js → crystal-DyWqUnlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-RYljyv7G.js → css-CQY0hFsD.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BnJXKHCY.js → dist-twkFmSUY.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-BobbycC-.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BJhNdQDc.js → edge-C1MwhJkX.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-g1AWOAfT.js → elixir-Bb3YbHfn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-CER5e4Dz.js → elm-DAN9IGQw.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-D5aOmMQf.js → erb-BEB8Xlsj.js} +1 -1
- package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-D6rfV8jp.js → git-rebase-tqpjRfxO.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-BvDH3-mC.js → glimmer-js-Ccbo65zR.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-DChtNs9V.js → glimmer-ts-B6WBVMpE.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-BHlsWrxf.js → glsl-tRec3Fcu.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-BsaBHb53.js → graphql-P8kbxT4F.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-0lbDosCd.js → hack-H9Zhkagy.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-BKtco2vr.js → haml-DtEnpn7Z.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-BE_cj04z.js → handlebars-DkgPfoAz.js} +1 -1
- package/dist/assets/web/dist/assets/{html-BNcr8EVd.js → html-D30RXpIs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-BsJt2Kei.js → html-derivative-DwozLrEx.js} +1 -1
- package/dist/assets/web/dist/assets/{http-BR5P8ER_.js → http-DXuzBAPm.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CtHalpWS.js → hurl-d1UUJIt_.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-B_s5tKyq.js +1 -0
- package/dist/assets/web/dist/assets/{java-BXVdhN11.js → java-DL0gWf34.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CyoShgNQ.js → javascript-Cl7vavnS.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-BQYZzHex.js → jinja-CfQOWMX9.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DqrUa1Eq.js → jison-KdYirlqm.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CQiTF9Jj.js → json-C9cDQ-Qj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-Y16MkCYQ.js → jsx-CRx5NItd.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-zn16YONe.js → julia-CmsQsZQl.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DcVeN_MN.js → just-CsM3Q8TE.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BNNoF-WP.js → latex-BXCh5YRX.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-DAi6qHzn.js → liquid-BF2vwK8p.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-BswS0axw.js → lua-DcMBATrl.js} +1 -1
- package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +1 -0
- package/dist/assets/web/dist/assets/{main-B3is4sk6.js → main-d-w-tIKt.js} +2 -2
- package/dist/assets/web/dist/assets/{marko-C-4hkmQZ.js → marko-88MndKvG.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-0ZaHUkS7.js → mdc-DJ4kVAd8.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-DBDg5tOR.js → nginx-CP6mRgtV.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BVMz49Yw.js → nim-QQfj3fpF.js} +1 -1
- package/dist/assets/web/dist/assets/{org-1hO_K6ya.js → org-Bke3eBzc.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-CVRQLvVk.js → perl-Bkh0N7Kz.js} +1 -1
- package/dist/assets/web/dist/assets/{php-K9nCrB9n.js → php-Dnv1Piya.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-CTxOcmO3.js → pug-D6peFFI7.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CmCkfG5q.js → qml-D8PEs-C-.js} +1 -1
- package/dist/assets/web/dist/assets/{r-pP47Xn1X.js → r-B1EL9b_j.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BpW6r3tC.js → razor-BsMTIh2b.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CINgdY4N.js → regexp-HhvC8spD.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-BZIUEq2Q.js → rst-D5paAxpg.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-nzAOxz6r.js → ruby-BrQwhLrl.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-QIS1bFth.js → sas-afot2B1o.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-BuhBBVNt.js → scss-Efm-mwuG.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CQc1vXbk.js → shellscript-BK0Vv5fT.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CoubCAUv.js → shellsession-BGqlMC7N.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-CPRzlder.js → soy-M0b4UwGM.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-W9krb8-9.js → sql-sxe6IE9j.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-C9m_EB8J.js +9 -0
- package/dist/assets/web/dist/assets/{stata-CuISJEC0.js → stata-nPF_ddLP.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CSet7584.js → surrealql-D_GrC6u7.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-ChXTSYwK.js → svelte-DwL1AtNP.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-Bm55v62k.js → templ-7s7LTDkc.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-CICMX8Gj.js → tex-Bx-5fMxe.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-CA1UzWyB.js → ts-tags-DUMJnke_.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-Dy04HNbv.js → tsx-BN8biPDe.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-BBHsVnVD.js → twig-8nIu84TN.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-Dvc-wVBT.js → typescript-BooSPq_S.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Dvlqx3_q.js → typst-CTBiBsem.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-zWi1MNU2.js → vue-C6Ft4Lea.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-D4YxT3An.js → vue-html-Ba36dD5D.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-0YE7uQlH.js → vue-vine-NkFexVo2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-gm-iksZB.js → xml-C_THnHXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D_W5BcrY.js → xsl-D8G5xqjY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-CHmZ21wZ.js → yaml-jAMIJzge.js} +1 -1
- package/dist/cli.js +4121 -1902
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BuHD-Bv7.js +0 -2
- package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CBBIjPOa.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-Ci1Ygqnc.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +0 -1
- package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DEN5bSQf.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-DP_zSemY.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-yji8a1wM.js +0 -9
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { ModelUsage, RunUsage } from "./runUsage.js";
|
|
2
|
+
|
|
3
|
+
// The price of a model's tokens (docs/reference/specs/costs.md): one table
|
|
4
|
+
// serves every dollar of token arithmetic — the open day's estimate from
|
|
5
|
+
// Anthropic's hourly usage report and the by-user report's run tokens. The
|
|
6
|
+
// list prices are Anthropic's, keyed by family and copied from the pricing
|
|
7
|
+
// page; re-check them when the page moves.
|
|
8
|
+
|
|
9
|
+
/** USD per million tokens of one kind, one model family. */
|
|
10
|
+
export interface AnthropicModelPrice {
|
|
11
|
+
input: number;
|
|
12
|
+
output: number;
|
|
13
|
+
cacheWrite5m: number;
|
|
14
|
+
cacheWrite1h: number;
|
|
15
|
+
cacheRead: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Anthropic list prices per model family (platform.claude.com/docs/en/about-claude/pricing;
|
|
19
|
+
* re-check when it moves). Keyed by the family id the usage report spells — a dated
|
|
20
|
+
* release (`claude-haiku-4-5-20251001`) resolves to its family through
|
|
21
|
+
* `anthropicPriceOf`. Only what the estimate needs: the open day priced at
|
|
22
|
+
* the rate it will bill at; the cost report remains the invoice. */
|
|
23
|
+
export const ANTHROPIC_PRICES: Record<string, AnthropicModelPrice> = {
|
|
24
|
+
"claude-fable-5-1": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 0.25 },
|
|
25
|
+
"claude-mythos-5-1": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 0.25 },
|
|
26
|
+
"claude-fable-5": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 1 },
|
|
27
|
+
"claude-mythos-5": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 1 },
|
|
28
|
+
"claude-opus-5": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
29
|
+
"claude-opus-4-8": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
30
|
+
"claude-opus-4-7": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
31
|
+
"claude-opus-4-6": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
32
|
+
"claude-opus-4-5": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
|
|
33
|
+
"claude-opus-4-1": { input: 15, output: 75, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
|
|
34
|
+
"claude-opus-4": { input: 15, output: 75, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
|
|
35
|
+
"claude-sonnet-5": { input: 2, output: 10, cacheWrite5m: 2.5, cacheWrite1h: 4, cacheRead: 0.2 },
|
|
36
|
+
"claude-sonnet-4-6": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
|
|
37
|
+
"claude-sonnet-4-5": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
|
|
38
|
+
"claude-sonnet-4": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
|
|
39
|
+
"claude-haiku-4-5": { input: 1, output: 5, cacheWrite5m: 1.25, cacheWrite1h: 2, cacheRead: 0.1 },
|
|
40
|
+
"claude-haiku-3-5": { input: 0.8, output: 4, cacheWrite5m: 1, cacheWrite1h: 1.6, cacheRead: 0.08 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const DATED_RELEASE_SUFFIX = /^-\d{8}$/;
|
|
44
|
+
|
|
45
|
+
/** The family prices of a model id: the id itself, or the id less a dated
|
|
46
|
+
* release suffix (`-YYYYMMDD`). Nothing else counts as "the same family" —
|
|
47
|
+
* `claude-fable-5-1` is not `claude-fable-5` with a suffix, and its cache
|
|
48
|
+
* reads bill differently. Unknown → undefined, never a guess. */
|
|
49
|
+
export function anthropicPriceOf(modelId: string): AnthropicModelPrice | undefined {
|
|
50
|
+
const exact = ANTHROPIC_PRICES[modelId];
|
|
51
|
+
if (exact) return exact;
|
|
52
|
+
for (const family of Object.keys(ANTHROPIC_PRICES)) {
|
|
53
|
+
if (modelId.startsWith(family) && DATED_RELEASE_SUFFIX.test(modelId.slice(family.length)))
|
|
54
|
+
return ANTHROPIC_PRICES[family];
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Token counts of one usage-report row, in the report's own kinds. */
|
|
60
|
+
export interface AnthropicTokens {
|
|
61
|
+
uncachedInput: number;
|
|
62
|
+
output: number;
|
|
63
|
+
cacheRead: number;
|
|
64
|
+
cacheWrite5m: number;
|
|
65
|
+
cacheWrite1h: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What those tokens cost at the family's list prices; undefined for a model
|
|
69
|
+
* the table does not know (the caller reports the tokens, never $0). */
|
|
70
|
+
export function anthropicTokensCostUsd(modelId: string, t: AnthropicTokens): number | undefined {
|
|
71
|
+
const p = anthropicPriceOf(modelId);
|
|
72
|
+
if (!p) return undefined;
|
|
73
|
+
return (
|
|
74
|
+
(t.uncachedInput * p.input +
|
|
75
|
+
t.output * p.output +
|
|
76
|
+
t.cacheRead * p.cacheRead +
|
|
77
|
+
t.cacheWrite5m * p.cacheWrite5m +
|
|
78
|
+
t.cacheWrite1h * p.cacheWrite1h) /
|
|
79
|
+
1_000_000
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- the operator's table: `costs.prices` over the list (costs.md item 4b) --------------
|
|
84
|
+
|
|
85
|
+
/** USD per million tokens of each kind a run's spans count, for one `<provider>/<model>` ref. */
|
|
86
|
+
export interface ModelPrice {
|
|
87
|
+
input: number;
|
|
88
|
+
output: number;
|
|
89
|
+
cacheRead: number;
|
|
90
|
+
cacheWrite: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `costs.prices`: the exact ref as the spans name it → its rates. */
|
|
94
|
+
export type ModelPriceTable = Readonly<Record<string, ModelPrice>>;
|
|
95
|
+
|
|
96
|
+
/** No table configured: the list alone prices. */
|
|
97
|
+
export const NO_PRICES: ModelPriceTable = Object.freeze({});
|
|
98
|
+
|
|
99
|
+
const PRICE_KINDS = ["input", "output", "cacheRead", "cacheWrite"] as const;
|
|
100
|
+
|
|
101
|
+
const isRate = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
102
|
+
|
|
103
|
+
/** `costs.prices` as config spells it. Absent → the empty table. A key is a
|
|
104
|
+
* `<provider>/<model>` ref; a value names all four kinds as finite dollars per
|
|
105
|
+
* million ≥ 0 (a kind left out would price at $0 in silence); anything else
|
|
106
|
+
* is refused by name. */
|
|
107
|
+
export function parseModelPrices(raw: unknown): ModelPriceTable {
|
|
108
|
+
if (raw === undefined || raw === null) return NO_PRICES;
|
|
109
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
110
|
+
throw new Error("costs.prices must be a mapping of <provider>/<model> → rates");
|
|
111
|
+
const out: Record<string, ModelPrice> = {};
|
|
112
|
+
for (const [ref, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
113
|
+
const slash = ref.indexOf("/");
|
|
114
|
+
if (slash <= 0 || slash === ref.length - 1)
|
|
115
|
+
throw new Error(`costs.prices.${ref} must be keyed <provider>/<model>, the ref a run's spans name`);
|
|
116
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
117
|
+
throw new Error(
|
|
118
|
+
`costs.prices.${ref} must be a mapping of { input, output, cacheRead, cacheWrite } in USD per million tokens`,
|
|
119
|
+
);
|
|
120
|
+
const v = value as Record<string, unknown>;
|
|
121
|
+
const price: Partial<ModelPrice> = {};
|
|
122
|
+
for (const kind of PRICE_KINDS) {
|
|
123
|
+
if (!isRate(v[kind]))
|
|
124
|
+
throw new Error(`costs.prices.${ref}.${kind} must be a finite number of USD per million tokens, 0 or more`);
|
|
125
|
+
price[kind] = v[kind];
|
|
126
|
+
}
|
|
127
|
+
out[ref] = price as ModelPrice;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The price of a ref: the configured table first (the exact ref), else the
|
|
133
|
+
* Anthropic list by family after the provider prefix is dropped — cache
|
|
134
|
+
* writes at the 5-minute rate, the one cache-write count a span carries —
|
|
135
|
+
* and undefined for a model neither knows (the caller reports the tokens,
|
|
136
|
+
* never $0). */
|
|
137
|
+
export function modelPriceOf(ref: string, prices: ModelPriceTable = NO_PRICES): ModelPrice | undefined {
|
|
138
|
+
const configured = prices[ref];
|
|
139
|
+
if (configured) return configured;
|
|
140
|
+
const list = anthropicPriceOf(modelIdOf(ref));
|
|
141
|
+
if (!list) return undefined;
|
|
142
|
+
return { input: list.input, output: list.output, cacheRead: list.cacheRead, cacheWrite: list.cacheWrite5m };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** What a model's counted tokens cost at a price, USD. */
|
|
146
|
+
export function modelUsageUsd(m: ModelUsage, p: ModelPrice): number {
|
|
147
|
+
return (
|
|
148
|
+
(m.inputTokens * p.input +
|
|
149
|
+
m.outputTokens * p.output +
|
|
150
|
+
m.cacheReadTokens * p.cacheRead +
|
|
151
|
+
m.cacheWriteTokens * p.cacheWrite) /
|
|
152
|
+
1_000_000
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- a run's tokens, priced -----------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/** One model's tokens, priced; `usd` is null for a model the price table does not know. */
|
|
159
|
+
export interface PricedModelUsage extends ModelUsage {
|
|
160
|
+
usd: number | null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `anthropic/claude-fable-5` → `claude-fable-5`: the spans name the provider, the price table the model. */
|
|
164
|
+
export const modelIdOf = (ref: string): string => (ref.includes("/") ? ref.slice(ref.indexOf("/") + 1) : ref);
|
|
165
|
+
|
|
166
|
+
/** A run's dollars as every surface prints them (costs.md item 4c): cents from a
|
|
167
|
+
* dollar up (`$1.24`), a tenth of a cent below that (`$0.038`), and `<$0.001`
|
|
168
|
+
* under that — a run that spent anything never reads as `$0.000`; `$0.00` is a
|
|
169
|
+
* run with no turns and nothing else. */
|
|
170
|
+
export function formatUsd(usd: number): string {
|
|
171
|
+
if (usd === 0) return "$0.00";
|
|
172
|
+
if (usd < 0.001) return "<$0.001";
|
|
173
|
+
return `$${usd.toFixed(usd >= 1 ? 2 : 3)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** What one run cost (costs.md item 4c): the dollars when every model it ran
|
|
177
|
+
* on has a price, null when one has none — a total that left a model's tokens
|
|
178
|
+
* out would understate the run — and $0 for a run with no turns at all.
|
|
179
|
+
* `byModel` says which model was unpriced. */
|
|
180
|
+
export interface RunCost {
|
|
181
|
+
usd: number | null;
|
|
182
|
+
byModel: Record<string, PricedModelUsage>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function runCostOf(usage: RunUsage, prices: ModelPriceTable = NO_PRICES): RunCost {
|
|
186
|
+
const priced = llmUsdOfUsage(usage, prices);
|
|
187
|
+
const unpriced = Object.values(priced.byModel).some((m) => m.usd === null);
|
|
188
|
+
return { usd: unpriced ? null : priced.usd, byModel: priced.byModel };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** A usage priced through the table (`modelPriceOf`): dollars for the models a
|
|
192
|
+
* price is known for, and the tokens of the ones it is not (never $0 in silence). */
|
|
193
|
+
export function llmUsdOfUsage(
|
|
194
|
+
usage: RunUsage,
|
|
195
|
+
prices: ModelPriceTable = NO_PRICES,
|
|
196
|
+
): {
|
|
197
|
+
usd: number;
|
|
198
|
+
unpricedTokens: number;
|
|
199
|
+
byModel: Record<string, PricedModelUsage>;
|
|
200
|
+
} {
|
|
201
|
+
let usd = 0;
|
|
202
|
+
let unpricedTokens = 0;
|
|
203
|
+
const byModel: Record<string, PricedModelUsage> = {};
|
|
204
|
+
for (const [ref, m] of Object.entries(usage.byModel)) {
|
|
205
|
+
const price = modelPriceOf(ref, prices);
|
|
206
|
+
const priced = price ? modelUsageUsd(m, price) : undefined;
|
|
207
|
+
if (priced === undefined) unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
|
|
208
|
+
else usd += priced;
|
|
209
|
+
byModel[ref] = { ...m, usd: priced ?? null };
|
|
210
|
+
}
|
|
211
|
+
return { usd, unpricedTokens, byModel };
|
|
212
|
+
}
|
|
@@ -6,49 +6,54 @@
|
|
|
6
6
|
// types and enforces (via its annotated parse return) that the zod output
|
|
7
7
|
// stays assignable to them. Add a field here first, then to the schema.
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* the PR head. The sha is NOT stored here — it is supplied at render
|
|
11
|
-
|
|
9
|
+
/** The lines a pointer sends the reader to: a path + inclusive 1-based line
|
|
10
|
+
* range in the PR head. The sha is NOT stored here — it is supplied at render
|
|
11
|
+
* time. */
|
|
12
|
+
export interface PrAnchor {
|
|
12
13
|
path: string;
|
|
13
14
|
from: number;
|
|
14
15
|
to: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
/** One
|
|
18
|
-
*
|
|
19
|
-
export interface
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
anchor:
|
|
18
|
+
/** One row of the map's "Where to look": a linked label, one sentence, an
|
|
19
|
+
* optional risk (rendered as ⚠), and the lines the label links to. */
|
|
20
|
+
export interface Pointer {
|
|
21
|
+
label: string;
|
|
22
|
+
text: string;
|
|
23
|
+
risk?: string;
|
|
24
|
+
anchor: PrAnchor;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
export interface
|
|
27
|
+
/** An anchor with the sha its permalink was rendered at — what a reader gets
|
|
28
|
+
* back from a rendered body (or from a submitted object at the head it was
|
|
29
|
+
* rendered for), so a surface can tell whether the pointers are at the head
|
|
30
|
+
* it is looking at. */
|
|
31
|
+
export interface RenderedPrAnchor extends PrAnchor {
|
|
31
32
|
sha: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
/** A
|
|
35
|
-
export interface
|
|
36
|
-
anchor:
|
|
35
|
+
/** A pointer whose anchor carries its render sha. Assignable to `Pointer`. */
|
|
36
|
+
export interface RenderedPointer extends Pointer {
|
|
37
|
+
anchor: RenderedPrAnchor;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/** The PR description as data (docs/decisions/0050): the map above the fold
|
|
41
|
+
* (tldr, why, pointers, feedbackWanted, risk, verified), every field capped
|
|
42
|
+
* so the map's size does not grow with the diff, and the collapsed half
|
|
43
|
+
* (decisions, validation, agentNotes) below it. */
|
|
39
44
|
export interface PrDescription {
|
|
40
45
|
/** The PR title's single source. Metadata for the PR's own title field —
|
|
41
46
|
* never rendered into the body (GitHub shows the title itself). */
|
|
42
47
|
title: string;
|
|
43
48
|
tldr: string;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
why: string;
|
|
50
|
+
pointers: Pointer[];
|
|
51
|
+
feedbackWanted: string;
|
|
52
|
+
risk: string;
|
|
53
|
+
verified: string;
|
|
48
54
|
decisions: { title: string; rationale: string }[];
|
|
49
|
-
risks: string;
|
|
50
55
|
validation: {
|
|
51
|
-
summary?: string;
|
|
52
56
|
criteria: { criterion: string; proof: string }[];
|
|
53
57
|
};
|
|
58
|
+
agentNotes?: string;
|
|
54
59
|
}
|
|
@@ -163,6 +163,13 @@ export function formatFinding(f: Finding): string {
|
|
|
163
163
|
return `[${f.severity}] ${f.id} ${location} — ${f.title}`;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/** One compact disposition line — `id: fixed|declined[ — note]` — the coding
|
|
167
|
+
* side's answer to a finding, as ship's re-review turn and the thread's
|
|
168
|
+
* artifacts block both render it. */
|
|
169
|
+
export function formatDisposition(d: FindingDisposition): string {
|
|
170
|
+
return `${d.findingId}: ${d.disposition}${d.note ? ` — ${d.note}` : ""}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
166
173
|
/**
|
|
167
174
|
* Build the body posted to GitHub: the deterministic verdict line, the
|
|
168
175
|
* compact findings list (when present), a blank line, then the model's
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Types only, and from the zod-free module deliberately: this file is part of
|
|
2
2
|
// the node-free contract the memory Worker and web app compile with their own
|
|
3
3
|
// tsconfigs — importing prDescription.ts would drag zod into those graphs.
|
|
4
|
-
import type { PrDescription,
|
|
4
|
+
import type { PrDescription, RenderedPointer } from "./prDescriptionTypes.js";
|
|
5
|
+
import type { HarnessScope } from "./harness/scope.js";
|
|
5
6
|
|
|
6
7
|
/** The `pr_description` review artifact minus the event envelope
|
|
7
8
|
* (docs/reference/specs/reading-diff.md item 7). */
|
|
@@ -25,8 +26,11 @@ export interface PrDescriptionArtifact {
|
|
|
25
26
|
/** The body as rendered (submitted) or as GitHub holds it (parsed), capped. */
|
|
26
27
|
body: string;
|
|
27
28
|
tldr?: string;
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
why?: string;
|
|
30
|
+
/** The map's "Where to look", each anchor stamped with the sha its permalink
|
|
31
|
+
* was rendered at. (A record written under the previous contract carries
|
|
32
|
+
* this array as `tour`; the line parser accepts either name.) */
|
|
33
|
+
pointers: RenderedPointer[];
|
|
30
34
|
decisions: { title: string; rationale: string }[];
|
|
31
35
|
/** Nothing missing or malformed — always true for `submitted`. */
|
|
32
36
|
complete: boolean;
|
|
@@ -97,6 +101,11 @@ export type RunNoteKind =
|
|
|
97
101
|
/** The PR head moved while a review ran and the same run is re-reviewing at
|
|
98
102
|
* the new head (agent-review.md item 12). Published by the dispatcher. */
|
|
99
103
|
| "head_moved"
|
|
104
|
+
/** The run loop threw and the run finishes `failed`: the summary is the
|
|
105
|
+
* error's message, redacted and capped, so the run page says why a failed
|
|
106
|
+
* run failed even when the reply is never delivered (run-history.md).
|
|
107
|
+
* Published by the run loop's catch, before the finish. */
|
|
108
|
+
| "run_failed"
|
|
100
109
|
/** An MCP server configured for this agent did not answer discovery
|
|
101
110
|
* (docs/reference/specs/mcp-tools.md item 8); the run proceeds without its tools. One
|
|
102
111
|
* note per server, published by the dispatcher before the first turn. */
|
|
@@ -187,6 +196,11 @@ export type RunNoteKind =
|
|
|
187
196
|
* item 7): the summary names the tool and the rule; the model read the same
|
|
188
197
|
* reason as the tool's result. Published by the bot's authorize route. */
|
|
189
198
|
| "tool_refused"
|
|
199
|
+
/** A ship coding child's budget ended with work still in the tree: the run
|
|
200
|
+
* loop committed and pushed it to the unit's branch (or says plainly that
|
|
201
|
+
* there was nothing to push), so a re-issue starts from the partial work
|
|
202
|
+
* (docs/reference/specs/agent-ship.md item 8). */
|
|
203
|
+
| "budget_salvage"
|
|
190
204
|
/** The native loop's stuck-loop guard: the same tool call failed identically
|
|
191
205
|
* six times in a row and the run was forced into its write-up. Written by
|
|
192
206
|
* no loop since that loop's deletion; a record from before it may carry it. */
|
|
@@ -205,6 +219,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
205
219
|
"stopped",
|
|
206
220
|
"spans_dropped",
|
|
207
221
|
"head_moved",
|
|
222
|
+
"run_failed",
|
|
208
223
|
"mcp_unavailable",
|
|
209
224
|
"follow_up",
|
|
210
225
|
"resumed",
|
|
@@ -220,6 +235,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
220
235
|
"harness_error",
|
|
221
236
|
"policy_refusal",
|
|
222
237
|
"tool_refused",
|
|
238
|
+
"budget_salvage",
|
|
223
239
|
"stuck_loop",
|
|
224
240
|
] as const satisfies readonly RunNoteKind[];
|
|
225
241
|
type _EveryKindListed = [RunNoteKind] extends [(typeof RUN_NOTE_KINDS)[number]] ? true : never;
|
|
@@ -375,6 +391,13 @@ export type RouteInputObject2 = { readonly [key: string]: RouteInputLeafOrList |
|
|
|
375
391
|
export type RouteInputObject3 = { readonly [key: string]: RouteInputLeafOrList | RouteInputObject2 };
|
|
376
392
|
export type RouteInputValue = RouteInputLeafOrList | RouteInputObject3;
|
|
377
393
|
|
|
394
|
+
/** How a door decision about a state change ended (docs/decisions/0044-a-routed-write-is-confirmed-in-proportion-to-its-blast-radius.md):
|
|
395
|
+
* `hand_back` — the router bound a state-changing command and the door answered
|
|
396
|
+
* the line to paste, nothing invoked; `pasted` — the typed line that followed
|
|
397
|
+
* a hand-back in its thread, with the same receipt, ran. A routed read
|
|
398
|
+
* carries no outcome: it is not a decision about a state change. */
|
|
399
|
+
export type RouteOutcome = "hand_back" | "pasted";
|
|
400
|
+
|
|
378
401
|
export type RunEvent =
|
|
379
402
|
/** `callId` is the provider's tool_use id — the explicit pair key between a
|
|
380
403
|
* call and its result (live-view item 13); the runner stamps it on both. */
|
|
@@ -519,9 +542,15 @@ export type RunEvent =
|
|
|
519
542
|
/** The request's trace id (docs/reference/specs/tracing.md), once the root exists. */
|
|
520
543
|
traceId?: string;
|
|
521
544
|
/** The harness the run is driven by (`Harness.name`; docs/reference/specs/harness.md
|
|
522
|
-
*
|
|
545
|
+
* items 8 and 10): the word the scopes resolved for the preset, `pi`
|
|
546
|
+
* when none named it. Absent on a command run, which starts no process,
|
|
523
547
|
* and on a record written before the seam existed. Additive. */
|
|
524
548
|
harness?: string;
|
|
549
|
+
/** Whose word put the run on that harness (item 10): the requester's own
|
|
550
|
+
* scope, the channel's, or the deployment's top-level block. Absent when
|
|
551
|
+
* no scope named the preset — the roster's default — and on records
|
|
552
|
+
* written before the word was a scope setting. */
|
|
553
|
+
harnessScope?: HarnessScope;
|
|
525
554
|
effort?: string;
|
|
526
555
|
repo?: string;
|
|
527
556
|
ref?: string;
|
|
@@ -654,6 +683,14 @@ export type RunEvent =
|
|
|
654
683
|
seq?: number;
|
|
655
684
|
at?: number;
|
|
656
685
|
}
|
|
686
|
+
/** The run's lease as the harness started it (docs/reference/specs/run-history.md
|
|
687
|
+
* item 2; decision 0046): when the wall clock began, when the lease ends,
|
|
688
|
+
* and when the loop ends — the lease's end less the write-up and the
|
|
689
|
+
* post-step the lease holds back — so a record says how long the run had
|
|
690
|
+
* and where its loop was cut. Published by the harness once, at the first
|
|
691
|
+
* start of the loop; a resumed run carries the original. Head material,
|
|
692
|
+
* like `run_meta`. Additive: unknown → ignored. */
|
|
693
|
+
| { type: "lease"; startedAt: number; endsAt: number; loopEndsAt: number; seq?: number; at?: number }
|
|
657
694
|
/** The coordinator tag as a fact of the run (docs/reference/specs/run-history.md
|
|
658
695
|
* item 48a): the instance the run is a child of, the unit its idempotency
|
|
659
696
|
* key named, and the base branch its pull request targets — published by
|
|
@@ -715,7 +752,11 @@ export type RunEvent =
|
|
|
715
752
|
* (`command`), `command` the id the model called, `input` the bound input
|
|
716
753
|
* (redacted, each value capped) and `receipt` the chat form the reply led
|
|
717
754
|
* with (`routed: <chat form>`, redacted and capped at `ROUTE_RECEIPT_CAP`);
|
|
718
|
-
* how the invoke ended is the run's own status and `answer`.
|
|
755
|
+
* how the invoke ended is the run's own status and `answer`. A door
|
|
756
|
+
* decision about a state change (record 0044) is a command run too, with
|
|
757
|
+
* `outcome` saying which: a hand-back invoked nothing and its `answer` is
|
|
758
|
+
* the line to paste; a paste is the typed line that followed, whatever its
|
|
759
|
+
* command, `handBackRunId` naming the hand-back's record. */
|
|
719
760
|
| {
|
|
720
761
|
type: "route";
|
|
721
762
|
preset: string;
|
|
@@ -726,6 +767,8 @@ export type RunEvent =
|
|
|
726
767
|
command?: string;
|
|
727
768
|
input?: { readonly [key: string]: RouteInputValue };
|
|
728
769
|
receipt?: string;
|
|
770
|
+
outcome?: RouteOutcome;
|
|
771
|
+
handBackRunId?: string;
|
|
729
772
|
seq?: number;
|
|
730
773
|
at?: number;
|
|
731
774
|
}
|
|
@@ -425,7 +425,8 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
425
425
|
ev.type === "review_posted" ||
|
|
426
426
|
ev.type === "ship_round" ||
|
|
427
427
|
ev.type === "route" ||
|
|
428
|
-
ev.type === "reference"
|
|
428
|
+
ev.type === "reference" ||
|
|
429
|
+
ev.type === "lease"
|
|
429
430
|
) {
|
|
430
431
|
sideFactEvents++;
|
|
431
432
|
return;
|
|
@@ -56,6 +56,8 @@ export interface LiveRunMeta {
|
|
|
56
56
|
/** The bound credential behind the person (authorization.md item 15): a
|
|
57
57
|
* resume or restart dispatches under ITS grants again, never the person's. */
|
|
58
58
|
authenticatedAs?: string;
|
|
59
|
+
/** The app that relayed the request for the person (authorization.md item 14): a resume or restart keeps app ∩ person at the gates. */
|
|
60
|
+
postedBy?: string;
|
|
59
61
|
effort?: string;
|
|
60
62
|
ref?: string;
|
|
61
63
|
headSha?: string;
|
|
@@ -109,6 +109,9 @@ export interface RunRecord {
|
|
|
109
109
|
receivedAt?: number;
|
|
110
110
|
sealedAt?: number;
|
|
111
111
|
replyOk?: boolean;
|
|
112
|
+
/** Why a `replyOk: false` reply was not delivered (e.g. "no channel to deliver
|
|
113
|
+
* to", run-history.md item 38). Absent when the reply was delivered or none was made. */
|
|
114
|
+
replyNote?: string;
|
|
112
115
|
stepCount?: number;
|
|
113
116
|
schema?: number;
|
|
114
117
|
status: RunStatus;
|
|
@@ -206,6 +209,10 @@ export interface RunRecord {
|
|
|
206
209
|
* (docs/reference/specs/resident-repos.md item 29) — read off the record,
|
|
207
210
|
* never off the reply's text. */
|
|
208
211
|
pr?: RunPullRequest;
|
|
212
|
+
/** The run's lease as the harness started it (item 2; decision 0046): the
|
|
213
|
+
* `lease` event, folded at the assembly. Present only on a run whose
|
|
214
|
+
* harness started a loop; a record written before the event lacks it. */
|
|
215
|
+
lease?: RunLease;
|
|
209
216
|
/** What the run cost in tokens, per model, summed from its `model.turn`
|
|
210
217
|
* spans at finish (`usageOfEvents`; docs/reference/specs/costs.md, cost by user).
|
|
211
218
|
* Every record written since carries it (zero turns included); one written
|
|
@@ -222,6 +229,24 @@ export interface RunPullRequest {
|
|
|
222
229
|
head?: string;
|
|
223
230
|
}
|
|
224
231
|
|
|
232
|
+
/** A run's lease as the record names it (item 2): when the wall clock
|
|
233
|
+
* began, when the lease ends, and where the loop was cut so the write-up
|
|
234
|
+
* and the post-step run inside it. */
|
|
235
|
+
export interface RunLease {
|
|
236
|
+
startedAt: number;
|
|
237
|
+
endsAt: number;
|
|
238
|
+
loopEndsAt: number;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The lease a run's events say its harness started — the first `lease`
|
|
242
|
+
* event; a resumed run's later generations publish none — or nothing. */
|
|
243
|
+
export function leaseOfEvents(events: readonly RunEvent[]): RunLease | undefined {
|
|
244
|
+
for (const e of events) {
|
|
245
|
+
if (e.type === "lease") return { startedAt: e.startedAt, endsAt: e.endsAt, loopEndsAt: e.loopEndsAt };
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
225
250
|
/** The pull request a run's events say it opened or edited — the last
|
|
226
251
|
* `pr_opened` wins, as an edit after an open names the same PR — or nothing. */
|
|
227
252
|
export function prOfEvents(events: readonly RunEvent[]): RunPullRequest | undefined {
|
|
@@ -462,6 +487,10 @@ export interface RunListOptions {
|
|
|
462
487
|
/** The runs one run spawned or that continue a thread it opened
|
|
463
488
|
* (`RunRecord.parentRunId`, item 46) — a conductor's children as one listing. */
|
|
464
489
|
parentRunId?: string;
|
|
490
|
+
/** The runs that name one pull request (`namesPullRequest`, item 58): the
|
|
491
|
+
* coding runs whose post-step opened or edited it and the review runs that
|
|
492
|
+
* posted to it — the findings ledger's runs as one listing. */
|
|
493
|
+
pr?: { repo: string; number: number };
|
|
465
494
|
/** What the ACTOR may see (authorization): the store predicate compiled
|
|
466
495
|
* from the policy, pushed down so no surface loads rows and filters after.
|
|
467
496
|
* Absent = no visibility constraint — only a caller that has already decided
|
|
@@ -574,6 +603,27 @@ export function matchesVisibility(
|
|
|
574
603
|
}
|
|
575
604
|
}
|
|
576
605
|
|
|
606
|
+
/** The pull request number a stored row names (item 58): the one its coding
|
|
607
|
+
* post-step opened or edited (`pr`), else the one its review posted to
|
|
608
|
+
* (`reviewPost.target`). Undefined for a run that named none — a review whose
|
|
609
|
+
* post was skipped included. The Worker's `pr_number` column is this value,
|
|
610
|
+
* written at `put` and backfilled from `summary_json` with the same rule. */
|
|
611
|
+
export function pullRequestNumberOf(row: Pick<RunListItem, "pr" | "reviewPost">): number | undefined {
|
|
612
|
+
if (row.pr !== undefined) return row.pr.number;
|
|
613
|
+
if (row.reviewPost?.posted) return row.reviewPost.target.number;
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** The one truth table for `RunListOptions.pr`: the row's repository is the
|
|
618
|
+
* pull request's and the number it names is the pull request's. The Worker
|
|
619
|
+
* applies the same test in SQL (`repo = ? AND pr_number = ?`). */
|
|
620
|
+
export function namesPullRequest(
|
|
621
|
+
row: Pick<RunListItem, "repo" | "pr" | "reviewPost">,
|
|
622
|
+
pr: { repo: string; number: number },
|
|
623
|
+
): boolean {
|
|
624
|
+
return row.repo === pr.repo && pullRequestNumberOf(row) === pr.number;
|
|
625
|
+
}
|
|
626
|
+
|
|
577
627
|
/** The list order (`finishedAt` desc, `id` desc) as a cursor predicate: true
|
|
578
628
|
* when `row` comes strictly after the cursor. Shared by `selectListItems`;
|
|
579
629
|
* the Worker applies the same predicate in SQL. */
|
|
@@ -783,6 +833,7 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
783
833
|
if (r[key] !== undefined && !isFiniteNumber(r[key])) return false;
|
|
784
834
|
}
|
|
785
835
|
if (r.replyOk !== undefined && typeof r.replyOk !== "boolean") return false;
|
|
836
|
+
if (r.replyNote !== undefined && typeof r.replyNote !== "string") return false;
|
|
786
837
|
for (const key of ["stepCount", "schema"] as const) {
|
|
787
838
|
if (r[key] !== undefined && (!isFiniteNumber(r[key]) || !Number.isInteger(r[key]) || (r[key] as number) < 0))
|
|
788
839
|
return false;
|