@coreplane/switchboard 1.242.0 → 1.244.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 +20 -1
- package/dist/assets/deploy/cloudflare-memory/worker.ts +234 -29
- package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +483 -75
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +3 -3
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +31 -35
- package/dist/assets/src/config/profile.ts +68 -3
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/types.ts +10 -0
- package/dist/assets/src/core/budgets.ts +349 -0
- package/dist/assets/src/core/coordinator/contract.ts +32 -0
- package/dist/assets/src/core/coordinator/driver.ts +75 -22
- package/dist/assets/src/core/costs.ts +9 -76
- package/dist/assets/src/core/delivery.ts +1 -1
- 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 +72 -5
- package/dist/assets/src/core/runFriction.ts +3 -1
- package/dist/assets/src/core/runRecord.ts +78 -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 +2 -1
- package/dist/assets/src/core/ship/coordinator.ts +312 -97
- package/dist/assets/src/core/ship/handoff.ts +9 -0
- package/dist/assets/src/core/ship/renewal.ts +155 -0
- package/dist/assets/src/core/trace/attrs.ts +11 -0
- package/dist/assets/src/core/trace/types.ts +4 -3
- package/dist/assets/src/execution/bashTimeout.ts +8 -5
- package/dist/assets/src/execution/residentRefresh.ts +52 -7
- package/dist/assets/src/execution/residentSteps.ts +1 -1
- package/dist/assets/src/execution/residentText.ts +8 -3
- package/dist/assets/src/mcp/registry.ts +3 -0
- package/dist/assets/web/dist/.vite/manifest.json +432 -429
- package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DTMBgnIW.js → ResidentDetailPage-C5USV_8P.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-ELoXHlFt.js → StatusDot-_Q_bmReh.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BoeFwYP2.js → Tooltip-Cafsgd4m.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
- package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BU5UivXC.js → dist-BtPs2AkQ.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
- package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
- package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +1 -0
- package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
- package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
- package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
- package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
- package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
- package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
- package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
- package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
- package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-g7ml86LM.js → sseReplay-DaRtc7nI.js} +5 -5
- package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
- package/dist/cli.js +5843 -2246
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CtmKhhOF.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CaDvXhzJ.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DXD2gLJk.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.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-B2fX10aW.css +0 -1
- package/dist/assets/web/dist/assets/main-D4EA1g6n.js +0 -28
|
@@ -11,7 +11,11 @@ providers:
|
|
|
11
11
|
anthropic:
|
|
12
12
|
type: anthropic
|
|
13
13
|
apiKeyEnv: ANTHROPIC_API_KEY
|
|
14
|
-
#
|
|
14
|
+
# OpenAI itself runs on the same compatible adapter: any preset's model may
|
|
15
|
+
# be openai/<model-id> anywhere a model is accepted. The adapter speaks Chat
|
|
16
|
+
# Completions, not the Responses API, sends no reasoning control (our effort
|
|
17
|
+
# tiers do not reach these models), and meters tokens without pricing them.
|
|
18
|
+
# A first-class OpenAI block is a plan's business, not this file's.
|
|
15
19
|
openai:
|
|
16
20
|
type: openai-compatible
|
|
17
21
|
baseUrl: https://api.openai.com/v1
|
|
@@ -65,10 +69,19 @@ defaults:
|
|
|
65
69
|
# clipped (the card and the run record say so); a preset whose identity or
|
|
66
70
|
# class is above a cap is refused by name before any executor exists. Absent →
|
|
67
71
|
# nothing is capped and every preset runs exactly its declared profile.
|
|
72
|
+
# A fourth field, `confirm`, is the front door's and caps no run: the first
|
|
73
|
+
# blast-radius class a command the router bound from prose is handed back at
|
|
74
|
+
# (`To run this: …`) instead of run — `write` (every write is handed back; a
|
|
75
|
+
# read or a repository's own test or build runs) or `destructive` (a write
|
|
76
|
+
# runs at once; only a destructive one is handed back). It intersects toward
|
|
77
|
+
# caution like the run axes — a channel or a user can only ask MORE than what
|
|
78
|
+
# is set here — and unset everywhere it is the built-in `write`. `exec` and
|
|
79
|
+
# `never` are refused by name.
|
|
68
80
|
# boundary:
|
|
69
81
|
# maxMinutes: 120
|
|
70
82
|
# maxIdentity: write
|
|
71
83
|
# machines: [none, blank, repo-cold, repo-resident]
|
|
84
|
+
# confirm: write
|
|
72
85
|
|
|
73
86
|
# PR reviews and the reading diff (docs/reference/specs/reading-diff.md). Every
|
|
74
87
|
# review records its full `git diff`; the ABRIDGED version (meat.dev, run on
|
|
@@ -436,6 +449,12 @@ workspaceDir: ./workspaces
|
|
|
436
449
|
# r2Buckets: # optional: bucket name → label for buckets not named <worker>-cache
|
|
437
450
|
# some-other-bucket: uploads
|
|
438
451
|
# anthropicWorkspaceId: wrkspc_... # this app's Anthropic workspace
|
|
452
|
+
# prices: # optional: USD per million tokens by <provider>/<model>, the ref a run's
|
|
453
|
+
# openai/gpt-5: # spans name — for a model the Anthropic list does not know, or to
|
|
454
|
+
# input: 1.25 # override the list; all four kinds named (a provider with no
|
|
455
|
+
# output: 10 # cache-write charge says 0). A model neither knows is "unpriced",
|
|
456
|
+
# cacheRead: 0.125 # never $0, on the costs page and on a run's own page
|
|
457
|
+
# cacheWrite: 0
|
|
439
458
|
|
|
440
459
|
# Delivery indicators: GET /delivery and `delivery report` (docs/reference/specs/delivery.md).
|
|
441
460
|
# Optional. Needs a GitHub credential (the App triple or GH_TOKEN); read-only on GitHub.
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
isRunSession,
|
|
26
26
|
isRunVisibilityFilter,
|
|
27
27
|
normalizeStored,
|
|
28
|
+
pullRequestNumberOf,
|
|
28
29
|
RETENTION_BOUNDS,
|
|
29
30
|
RUN_EVENTS_DEFAULT_PAGE,
|
|
30
31
|
RUN_EVENTS_MAX_PAGE,
|
|
@@ -59,11 +60,11 @@ import {
|
|
|
59
60
|
} from "../../src/core/runLedger/sessionLog.ts";
|
|
60
61
|
import type { RunEvent } from "../../src/core/runEvents.ts";
|
|
61
62
|
import {
|
|
62
|
-
aggregateUsageByUser,
|
|
63
63
|
isRunUsage,
|
|
64
64
|
usageOfEvents,
|
|
65
65
|
type RunUsage,
|
|
66
|
-
type
|
|
66
|
+
type RunUsageRows,
|
|
67
|
+
type UsageIdentity,
|
|
67
68
|
type UsageRun,
|
|
68
69
|
} from "../../src/core/runUsage.ts";
|
|
69
70
|
import {
|
|
@@ -626,6 +627,27 @@ export class ScheduleDO extends DurableObject<Env> {
|
|
|
626
627
|
// for optimistic concurrency: a `put` whose `expectedVersion` is stale is a 409,
|
|
627
628
|
// never a silent clobber (the bot and the CLI both write this document).
|
|
628
629
|
|
|
630
|
+
/** What the bot posts to mint a confirmation: the facts this object judges on
|
|
631
|
+
* and the bot's own row as an opaque body (routing-and-config item 25). */
|
|
632
|
+
interface ConfirmationInput {
|
|
633
|
+
id: string;
|
|
634
|
+
threadKey: string;
|
|
635
|
+
requester: string;
|
|
636
|
+
body: Record<string, unknown>;
|
|
637
|
+
}
|
|
638
|
+
/** A stored confirmation as a consume returns it: the input plus the expiry this object stamped. */
|
|
639
|
+
interface ConfirmationRow extends ConfirmationInput {
|
|
640
|
+
expiresAt: number;
|
|
641
|
+
}
|
|
642
|
+
/** Why a consume or a cancel refused: the row is gone (`used`), past its expiry (`expired`), or someone else's (`foreign`). */
|
|
643
|
+
type ConfirmationRefusal = "used" | "expired" | "foreign";
|
|
644
|
+
type ConfirmationOutcome = { row: ConfirmationRow } | { refused: ConfirmationRefusal };
|
|
645
|
+
type ConfirmationCancelOutcome = { ok: true } | { refused: Exclude<ConfirmationRefusal, "expired"> };
|
|
646
|
+
|
|
647
|
+
function isJsonObject(v: unknown): v is Record<string, unknown> {
|
|
648
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
649
|
+
}
|
|
650
|
+
|
|
629
651
|
export class ConfigDO extends DurableObject<Env> {
|
|
630
652
|
private readonly sql: SqlStorage;
|
|
631
653
|
|
|
@@ -649,9 +671,91 @@ export class ConfigDO extends DurableObject<Env> {
|
|
|
649
671
|
expires_at INTEGER NOT NULL,
|
|
650
672
|
ticket TEXT NOT NULL
|
|
651
673
|
);
|
|
674
|
+
CREATE TABLE IF NOT EXISTS confirmations (
|
|
675
|
+
id TEXT PRIMARY KEY,
|
|
676
|
+
thread_key TEXT NOT NULL,
|
|
677
|
+
requester TEXT NOT NULL,
|
|
678
|
+
expires_at INTEGER NOT NULL,
|
|
679
|
+
body TEXT NOT NULL
|
|
680
|
+
);
|
|
652
681
|
`);
|
|
653
682
|
}
|
|
654
683
|
|
|
684
|
+
// ---- Confirmations (docs/reference/specs/routing-and-config.md item 25): the
|
|
685
|
+
// row a routed write is offered as, minted by the bot and consumed once at
|
|
686
|
+
// the click. The body is the bot's row, opaque here; this object owns the
|
|
687
|
+
// three facts the consume decides on — the thread (one pending row per
|
|
688
|
+
// thread), the requester (the click must be theirs) and the expiry, stamped
|
|
689
|
+
// and judged on this object's clock so two bot processes alive during a roll
|
|
690
|
+
// read one answer. Nothing sweeps: expiry is checked on touch.
|
|
691
|
+
|
|
692
|
+
/** Insert the row and drop the thread's older one in one transaction; the
|
|
693
|
+
* expiry is `now + ttlMs`, stamped here and answered to the caller. */
|
|
694
|
+
async putConfirmation(row: ConfirmationInput, ttlMs: number, now: number): Promise<number> {
|
|
695
|
+
const expiresAt = now + ttlMs;
|
|
696
|
+
this.ctx.storage.transactionSync(() => {
|
|
697
|
+
this.sql.exec(`DELETE FROM confirmations WHERE thread_key = ?`, row.threadKey);
|
|
698
|
+
this.sql.exec(
|
|
699
|
+
`INSERT OR REPLACE INTO confirmations (id, thread_key, requester, expires_at, body) VALUES (?, ?, ?, ?, ?)`,
|
|
700
|
+
row.id,
|
|
701
|
+
row.threadKey,
|
|
702
|
+
row.requester,
|
|
703
|
+
expiresAt,
|
|
704
|
+
JSON.stringify(row.body),
|
|
705
|
+
);
|
|
706
|
+
});
|
|
707
|
+
return expiresAt;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Read, judge and delete in one transaction on the single-threaded object,
|
|
711
|
+
* so a confirmation runs at most once whatever the click timing: a missing
|
|
712
|
+
* row is `used`, a row past its expiry is deleted and `expired`, a requester
|
|
713
|
+
* none of `actorIds` names is `foreign` (the row stays for its requester),
|
|
714
|
+
* and otherwise the row is deleted and returned. */
|
|
715
|
+
async consumeConfirmation(id: string, actorIds: readonly string[], now: number): Promise<ConfirmationOutcome> {
|
|
716
|
+
return this.ctx.storage.transactionSync(() => {
|
|
717
|
+
const stored = this.readConfirmation(id);
|
|
718
|
+
if (!stored) return { refused: "used" };
|
|
719
|
+
if (stored.expiresAt <= now) {
|
|
720
|
+
this.sql.exec(`DELETE FROM confirmations WHERE id = ?`, id);
|
|
721
|
+
return { refused: "expired" };
|
|
722
|
+
}
|
|
723
|
+
if (!actorIds.includes(stored.requester)) return { refused: "foreign" };
|
|
724
|
+
this.sql.exec(`DELETE FROM confirmations WHERE id = ?`, id);
|
|
725
|
+
return { row: stored };
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Delete the row under the same requester check as a consume; a missing row
|
|
730
|
+
* is `used`. Expiry plays no part: cancelling an expired offer still leaves
|
|
731
|
+
* nothing pending, which is what the click asked for. */
|
|
732
|
+
async cancelConfirmation(id: string, actorIds: readonly string[]): Promise<ConfirmationCancelOutcome> {
|
|
733
|
+
return this.ctx.storage.transactionSync(() => {
|
|
734
|
+
const stored = this.readConfirmation(id);
|
|
735
|
+
if (!stored) return { refused: "used" };
|
|
736
|
+
if (!actorIds.includes(stored.requester)) return { refused: "foreign" };
|
|
737
|
+
this.sql.exec(`DELETE FROM confirmations WHERE id = ?`, id);
|
|
738
|
+
return { ok: true };
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private readConfirmation(id: string): ConfirmationRow | undefined {
|
|
743
|
+
const row = this.sql
|
|
744
|
+
.exec<{ thread_key: string; requester: string; expires_at: number; body: string }>(
|
|
745
|
+
`SELECT thread_key, requester, expires_at, body FROM confirmations WHERE id = ?`,
|
|
746
|
+
id,
|
|
747
|
+
)
|
|
748
|
+
.toArray()[0];
|
|
749
|
+
if (!row) return undefined;
|
|
750
|
+
return {
|
|
751
|
+
id,
|
|
752
|
+
threadKey: row.thread_key,
|
|
753
|
+
requester: row.requester,
|
|
754
|
+
expiresAt: row.expires_at,
|
|
755
|
+
body: parseStored(row.body, isJsonObject) ?? {},
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
655
759
|
// ---- MCP sealed credentials + connect tickets (docs/reference/specs/mcp-tools.md items 15–16).
|
|
656
760
|
// Ciphertext the bot sealed — opaque here — and one-time tickets: the two
|
|
657
761
|
// things a config document must never carry, kept beside it on this object.
|
|
@@ -1051,8 +1155,21 @@ const CONFIG_ROUTES = new Set([
|
|
|
1051
1155
|
"/config/tickets/put",
|
|
1052
1156
|
"/config/tickets/get",
|
|
1053
1157
|
"/config/tickets/transition",
|
|
1158
|
+
"/config/confirmations/put",
|
|
1159
|
+
"/config/confirmations/consume",
|
|
1160
|
+
"/config/confirmations/cancel",
|
|
1054
1161
|
]);
|
|
1055
1162
|
const TICKET_STATES: ReadonlySet<string> = new Set<McpTicketState>(MCP_TICKET_STATES);
|
|
1163
|
+
/** A confirmation id as the bot mints it (a UUID) — one token, no whitespace, bounded. */
|
|
1164
|
+
const CONFIRMATION_ID_RE = /^[A-Za-z0-9_-]{8,128}$/;
|
|
1165
|
+
|
|
1166
|
+
/** The `{ id, actorIds }` a consume or a cancel carries, or the 400 that refuses it. */
|
|
1167
|
+
function confirmationClickOf(b: Record<string, unknown>): { id: string; actorIds: string[] } | Response {
|
|
1168
|
+
if (typeof b.id !== "string" || !CONFIRMATION_ID_RE.test(b.id)) return json({ error: "id malformed" }, 400);
|
|
1169
|
+
if (!Array.isArray(b.actorIds) || !b.actorIds.every((a): a is string => typeof a === "string"))
|
|
1170
|
+
return json({ error: "actorIds must be a list of actor ids" }, 400);
|
|
1171
|
+
return { id: b.id, actorIds: b.actorIds };
|
|
1172
|
+
}
|
|
1056
1173
|
|
|
1057
1174
|
async function handleConfig(pathname: string, body: unknown, env: Env): Promise<Response> {
|
|
1058
1175
|
const b = (typeof body === "object" && body !== null ? body : {}) as Record<string, unknown>;
|
|
@@ -1094,6 +1211,38 @@ async function handleConfig(pathname: string, body: unknown, env: Env): Promise<
|
|
|
1094
1211
|
);
|
|
1095
1212
|
return json({ ok: true, applied });
|
|
1096
1213
|
}
|
|
1214
|
+
// Confirmations (routing-and-config item 25): the body is the bot's row,
|
|
1215
|
+
// stored verbatim; the expiry is this object's clock plus the ttl the bot
|
|
1216
|
+
// passed, never a timestamp the bot chose.
|
|
1217
|
+
case "/config/confirmations/put": {
|
|
1218
|
+
if (typeof b.id !== "string" || !CONFIRMATION_ID_RE.test(b.id)) return json({ error: "id malformed" }, 400);
|
|
1219
|
+
if (typeof b.threadKey !== "string" || !b.threadKey) return json({ error: "threadKey required" }, 400);
|
|
1220
|
+
if (typeof b.requester !== "string" || !b.requester) return json({ error: "requester required" }, 400);
|
|
1221
|
+
if (!isJsonObject(b.body)) return json({ error: "body must be a JSON object" }, 400);
|
|
1222
|
+
if (typeof b.ttlMs !== "number" || !Number.isInteger(b.ttlMs) || b.ttlMs < 0)
|
|
1223
|
+
return json({ error: "ttlMs must be a non-negative integer" }, 400);
|
|
1224
|
+
const expiresAt = await dO.putConfirmation(
|
|
1225
|
+
{ id: b.id, threadKey: b.threadKey, requester: b.requester, body: b.body },
|
|
1226
|
+
b.ttlMs,
|
|
1227
|
+
systemClock(),
|
|
1228
|
+
);
|
|
1229
|
+
console.log(`[config/confirmations/put] ${b.threadKey} ${b.id} expires_at=${expiresAt}`);
|
|
1230
|
+
return json({ ok: true, expiresAt });
|
|
1231
|
+
}
|
|
1232
|
+
case "/config/confirmations/consume": {
|
|
1233
|
+
const click = confirmationClickOf(b);
|
|
1234
|
+
if (click instanceof Response) return click;
|
|
1235
|
+
const outcome = await dO.consumeConfirmation(click.id, click.actorIds, systemClock());
|
|
1236
|
+
console.log(`[config/confirmations/consume] ${click.id} ${"row" in outcome ? "consumed" : outcome.refused}`);
|
|
1237
|
+
return json(outcome);
|
|
1238
|
+
}
|
|
1239
|
+
case "/config/confirmations/cancel": {
|
|
1240
|
+
const click = confirmationClickOf(b);
|
|
1241
|
+
if (click instanceof Response) return click;
|
|
1242
|
+
const outcome = await dO.cancelConfirmation(click.id, click.actorIds);
|
|
1243
|
+
console.log(`[config/confirmations/cancel] ${click.id} ${"ok" in outcome ? "cancelled" : outcome.refused}`);
|
|
1244
|
+
return json(outcome);
|
|
1245
|
+
}
|
|
1097
1246
|
default:
|
|
1098
1247
|
break;
|
|
1099
1248
|
}
|
|
@@ -1265,13 +1414,15 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1265
1414
|
this.migrateRunsTable();
|
|
1266
1415
|
// The indexes the visibility predicate's leaves walk (`channel_id IN`,
|
|
1267
1416
|
// `channel_visibility IN`, `user_id =`), each ordered like the page; the
|
|
1268
|
-
// session the sweep asks about; the parent a children listing filters on
|
|
1417
|
+
// session the sweep asks about; the parent a children listing filters on;
|
|
1418
|
+
// the pull request a findings listing filters on.
|
|
1269
1419
|
this.sql.exec(`
|
|
1270
1420
|
CREATE INDEX IF NOT EXISTS runs_channel_finished ON runs(channel_id, finished_at DESC, run_id DESC);
|
|
1271
1421
|
CREATE INDEX IF NOT EXISTS runs_visibility_finished ON runs(channel_visibility, finished_at DESC, run_id DESC);
|
|
1272
1422
|
CREATE INDEX IF NOT EXISTS runs_user_finished ON runs(user_id, finished_at DESC, run_id DESC);
|
|
1273
1423
|
CREATE INDEX IF NOT EXISTS runs_session ON runs(session_key);
|
|
1274
1424
|
CREATE INDEX IF NOT EXISTS runs_parent ON runs(parent_run_id, finished_at DESC, run_id DESC);
|
|
1425
|
+
CREATE INDEX IF NOT EXISTS runs_pr ON runs(repo, pr_number, finished_at DESC, run_id DESC);
|
|
1275
1426
|
`);
|
|
1276
1427
|
// The sessions registry (session-log item 7): every session log a run of
|
|
1277
1428
|
// this store claimed, with its thread — the sweep cannot enumerate the
|
|
@@ -1438,7 +1589,11 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1438
1589
|
* aggregate fills it from the run's stored events. The parent a child names
|
|
1439
1590
|
* (run-history item 46), the column a children listing filters on: the
|
|
1440
1591
|
* record already carries it in `summary_json`, so existing rows are filled
|
|
1441
|
-
* from there once, and every later `put` writes it beside the row.
|
|
1592
|
+
* from there once, and every later `put` writes it beside the row. The pull
|
|
1593
|
+
* request a run names (run-history item 58; `pullRequestNumberOf`), the
|
|
1594
|
+
* column a findings listing filters on: the same one-time fill from
|
|
1595
|
+
* `summary_json` — the coding post-step's `pr`, else a posted review's
|
|
1596
|
+
* target — and every later `put` writes it beside the row. */
|
|
1442
1597
|
migrateRunsTable(): void {
|
|
1443
1598
|
const columns = new Set(
|
|
1444
1599
|
this.sql
|
|
@@ -1457,6 +1612,19 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1457
1612
|
WHERE json_type(summary_json, '$.parentRunId') = 'text'`,
|
|
1458
1613
|
);
|
|
1459
1614
|
}
|
|
1615
|
+
if (!columns.has("pr_number")) {
|
|
1616
|
+
this.sql.exec(`ALTER TABLE runs ADD COLUMN pr_number INTEGER`);
|
|
1617
|
+
// `pullRequestNumberOf` in SQL: the coding post-step's pull request, else
|
|
1618
|
+
// the one a posted review targeted (a skipped post has no target).
|
|
1619
|
+
this.sql.exec(
|
|
1620
|
+
`UPDATE runs SET pr_number = COALESCE(
|
|
1621
|
+
CASE WHEN json_type(summary_json, '$.pr.number') = 'integer' THEN json_extract(summary_json, '$.pr.number') END,
|
|
1622
|
+
CASE WHEN json_extract(summary_json, '$.reviewPost.posted') = 1
|
|
1623
|
+
AND json_type(summary_json, '$.reviewPost.target.number') = 'integer'
|
|
1624
|
+
THEN json_extract(summary_json, '$.reviewPost.target.number') END
|
|
1625
|
+
)`,
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1460
1628
|
}
|
|
1461
1629
|
|
|
1462
1630
|
private liveByThread(threadKey: string): LiveRunRow | undefined {
|
|
@@ -1970,8 +2138,8 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1970
2138
|
);
|
|
1971
2139
|
this.sql.exec(
|
|
1972
2140
|
`INSERT INTO runs (run_id, label, agent, model, channel_id, user_id, thread_key, channel_visibility, repo, started_at, finished_at, stored_at, status,
|
|
1973
|
-
event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json, parent_run_id)
|
|
1974
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2141
|
+
event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json, parent_run_id, pr_number)
|
|
2142
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1975
2143
|
ON CONFLICT(run_id) DO UPDATE SET
|
|
1976
2144
|
label = excluded.label, agent = excluded.agent, model = excluded.model, channel_id = excluded.channel_id,
|
|
1977
2145
|
user_id = excluded.user_id, thread_key = excluded.thread_key, channel_visibility = excluded.channel_visibility,
|
|
@@ -1981,7 +2149,8 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1981
2149
|
bytes = excluded.bytes, diagnosis_json = excluded.diagnosis_json, summary_json = excluded.summary_json,
|
|
1982
2150
|
session_key = excluded.session_key,
|
|
1983
2151
|
usage_json = COALESCE(excluded.usage_json, runs.usage_json),
|
|
1984
|
-
parent_run_id = excluded.parent_run_id
|
|
2152
|
+
parent_run_id = excluded.parent_run_id,
|
|
2153
|
+
pr_number = excluded.pr_number`,
|
|
1985
2154
|
stored.id,
|
|
1986
2155
|
stored.label ?? null,
|
|
1987
2156
|
stored.agent ?? null,
|
|
@@ -2004,6 +2173,7 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2004
2173
|
stored.session?.key ?? null,
|
|
2005
2174
|
stored.usage ? JSON.stringify(stored.usage) : null,
|
|
2006
2175
|
stored.parentRunId ?? null,
|
|
2176
|
+
pullRequestNumberOf(stored) ?? null,
|
|
2007
2177
|
);
|
|
2008
2178
|
// The session's registry row learns its newest finish (session-log item
|
|
2009
2179
|
// 7); a record that reaches the store without a claim (the plain put
|
|
@@ -2208,28 +2378,32 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2208
2378
|
}
|
|
2209
2379
|
|
|
2210
2380
|
/**
|
|
2211
|
-
*
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2381
|
+
* What the runs that finished in [sinceMs, untilMs) and are inside the
|
|
2382
|
+
* retention window cost (costs.md items 10–10a; run-history item 56): one row
|
|
2383
|
+
* per run — its requester, thread, channel, agent and usage — plus the
|
|
2384
|
+
* identity of every parent a child names that is outside the batch, so the
|
|
2385
|
+
* bot bills the child without a second read. The arithmetic is the bot's; the
|
|
2386
|
+
* Worker only reads its rows. A row written before `usage_json` existed is
|
|
2387
|
+
* filled in here from its stored `model.turn` events — up to
|
|
2388
|
+
* USAGE_BACKFILL_PER_CALL a call, the rest answered without `usage` and
|
|
2389
|
+
* counted in `pending` — so the history heals as it is read, with no
|
|
2216
2390
|
* operator step; a run without turns is written back as the zero usage so it
|
|
2217
|
-
* is not re-read.
|
|
2218
|
-
* outside the range is looked up by id.
|
|
2391
|
+
* is not re-read.
|
|
2219
2392
|
*/
|
|
2220
|
-
|
|
2393
|
+
usage(sinceMs: number, untilMs: number): RunUsageRows {
|
|
2221
2394
|
const now = systemClock();
|
|
2222
2395
|
const { policy } = this.policyState();
|
|
2223
2396
|
const cutoff = now - policy.retentionDays * 86_400_000;
|
|
2224
2397
|
const rows = this.sql
|
|
2225
2398
|
.exec<UsageRow>(
|
|
2226
|
-
`SELECT run_id, user_id, started_at, finished_at, usage_json, summary_json FROM runs
|
|
2399
|
+
`SELECT run_id, user_id, agent, channel_id, thread_key, started_at, finished_at, usage_json, summary_json FROM runs
|
|
2227
2400
|
WHERE finished_at >= ? AND finished_at < ? ORDER BY finished_at ASC, run_id ASC`,
|
|
2228
2401
|
Math.max(sinceMs, cutoff),
|
|
2229
2402
|
untilMs,
|
|
2230
2403
|
)
|
|
2231
2404
|
.toArray();
|
|
2232
2405
|
let backfilled = 0;
|
|
2406
|
+
let pending = 0;
|
|
2233
2407
|
const runs: UsageRun[] = rows.map((row) => {
|
|
2234
2408
|
let usage = parseUsageJson(row.usage_json);
|
|
2235
2409
|
if (usage === undefined && backfilled < USAGE_BACKFILL_PER_CALL) {
|
|
@@ -2237,31 +2411,38 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2237
2411
|
this.sql.exec(`UPDATE runs SET usage_json = ? WHERE run_id = ?`, JSON.stringify(usage), row.run_id);
|
|
2238
2412
|
backfilled += 1;
|
|
2239
2413
|
}
|
|
2414
|
+
if (usage === undefined) pending += 1;
|
|
2240
2415
|
const who = identityOfSummary(row.summary_json);
|
|
2241
2416
|
return {
|
|
2242
2417
|
id: row.run_id,
|
|
2243
2418
|
userId: row.user_id,
|
|
2244
2419
|
...(who.userName ? { userName: who.userName } : {}),
|
|
2245
2420
|
...(who.parentRunId ? { parentRunId: who.parentRunId } : {}),
|
|
2421
|
+
threadKey: row.thread_key,
|
|
2422
|
+
channelId: row.channel_id,
|
|
2423
|
+
...(row.agent ? { agent: row.agent } : {}),
|
|
2246
2424
|
startedAt: row.started_at,
|
|
2247
2425
|
finishedAt: row.finished_at,
|
|
2248
2426
|
...(usage ? { usage } : {}),
|
|
2249
2427
|
};
|
|
2250
2428
|
});
|
|
2251
|
-
|
|
2429
|
+
// The parents a child names that are not in the batch: looked up once each, known or not.
|
|
2430
|
+
const inBatch = new Set(runs.map((r) => r.id));
|
|
2431
|
+
const parents: Record<string, UsageIdentity> = {};
|
|
2432
|
+
for (const id of new Set(runs.map((r) => r.parentRunId).filter((p): p is string => !!p && !inBatch.has(p)))) {
|
|
2252
2433
|
const p = this.sql
|
|
2253
2434
|
.exec<{ user_id: string; summary_json: string }>(`SELECT user_id, summary_json FROM runs WHERE run_id = ?`, id)
|
|
2254
2435
|
.toArray()[0];
|
|
2255
|
-
if (!p)
|
|
2436
|
+
if (!p) continue;
|
|
2256
2437
|
const who = identityOfSummary(p.summary_json);
|
|
2257
|
-
|
|
2258
|
-
}
|
|
2259
|
-
const { rows: out, pending } = aggregateUsageByUser(runs, lookupParent);
|
|
2438
|
+
parents[id] = { userId: p.user_id, ...(who.userName ? { userName: who.userName } : {}) };
|
|
2439
|
+
}
|
|
2260
2440
|
const earliest = this.sql
|
|
2261
2441
|
.exec<{ m: number | null }>(`SELECT MIN(finished_at) AS m FROM runs WHERE finished_at >= ?`, cutoff)
|
|
2262
2442
|
.one().m;
|
|
2263
2443
|
return {
|
|
2264
|
-
|
|
2444
|
+
runs,
|
|
2445
|
+
parents,
|
|
2265
2446
|
pending,
|
|
2266
2447
|
...(earliest !== null ? { earliestFinishedAt: earliest } : {}),
|
|
2267
2448
|
retentionDays: policy.retentionDays,
|
|
@@ -2343,6 +2524,11 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2343
2524
|
where.push(`parent_run_id = ?`);
|
|
2344
2525
|
params.push(q.parentRunId);
|
|
2345
2526
|
}
|
|
2527
|
+
if (q.pr !== undefined) {
|
|
2528
|
+
// `namesPullRequest` in SQL: the row's repository and the number it names.
|
|
2529
|
+
where.push(`repo = ?`, `pr_number = ?`);
|
|
2530
|
+
params.push(q.pr.repo, q.pr.number);
|
|
2531
|
+
}
|
|
2346
2532
|
if (q.visibleTo !== undefined && q.visibleTo.kind !== "all") where.push(visibilitySql(q.visibleTo, params));
|
|
2347
2533
|
const select = `SELECT run_id, agent, channel_id, finished_at, bytes, event_count, summary_json FROM runs WHERE ${where.join(" AND ")} ORDER BY finished_at DESC, run_id DESC`;
|
|
2348
2534
|
// `LIMIT` holds on the over-bound path too: the kept set is the newest
|
|
@@ -2423,6 +2609,9 @@ const USAGE_BACKFILL_PER_CALL = 200;
|
|
|
2423
2609
|
type UsageRow = {
|
|
2424
2610
|
run_id: string;
|
|
2425
2611
|
user_id: string;
|
|
2612
|
+
agent: string | null;
|
|
2613
|
+
channel_id: string;
|
|
2614
|
+
thread_key: string;
|
|
2426
2615
|
started_at: number;
|
|
2427
2616
|
finished_at: number;
|
|
2428
2617
|
usage_json: string | null;
|
|
@@ -2590,12 +2779,26 @@ function parseRunList(body: unknown): Validated<{ storeKey: string; query: RunLi
|
|
|
2590
2779
|
if (!id.ok) return invalid("parentRunId must match ^[A-Za-z0-9_-]{1,64}$");
|
|
2591
2780
|
query.parentRunId = id.value;
|
|
2592
2781
|
}
|
|
2782
|
+
if (b.pr !== undefined) {
|
|
2783
|
+
const pr = b.pr as Record<string, unknown> | null;
|
|
2784
|
+
if (
|
|
2785
|
+
typeof pr !== "object" ||
|
|
2786
|
+
pr === null ||
|
|
2787
|
+
typeof pr.repo !== "string" ||
|
|
2788
|
+
!REPO_SLUG.test(pr.repo) ||
|
|
2789
|
+
typeof pr.number !== "number" ||
|
|
2790
|
+
!Number.isInteger(pr.number) ||
|
|
2791
|
+
pr.number < 1
|
|
2792
|
+
)
|
|
2793
|
+
return invalid("pr must be { repo: owner/name, number: a positive integer }");
|
|
2794
|
+
query.pr = { repo: pr.repo, number: pr.number };
|
|
2795
|
+
}
|
|
2593
2796
|
if (b.visibleTo !== undefined) {
|
|
2594
2797
|
// A malformed filter is a 400, never "all": the bot degrades to live rows
|
|
2595
2798
|
// rather than the DO widening what an actor may see.
|
|
2596
2799
|
if (!isRunVisibilityFilter(b.visibleTo)) return invalid("visibleTo must be a run visibility filter");
|
|
2597
|
-
|
|
2598
|
-
|
|
2800
|
+
const headroom = DO_MAX_BOUND_PARAMETERS - RUN_LIST_BASE_PARAMETERS - (query.pr ? RUN_LIST_PR_PARAMETERS : 0);
|
|
2801
|
+
if (boundParameters(b.visibleTo) > headroom) return invalid(`visibleTo names more than ${headroom} ids`);
|
|
2599
2802
|
query.visibleTo = b.visibleTo;
|
|
2600
2803
|
}
|
|
2601
2804
|
return { ok: true, value: { storeKey: key.value, query } };
|
|
@@ -2604,6 +2807,8 @@ function parseRunList(body: unknown): Validated<{ storeKey: string; query: RunLi
|
|
|
2604
2807
|
/** Parameters the page query binds before any filter: the cursor pair (3) and the age floor (1),
|
|
2605
2808
|
* plus `agent`, `channel`, `threadKey`, `parentRunId`, and the LIMIT at most — the headroom `visibleTo` must fit under. */
|
|
2606
2809
|
const RUN_LIST_BASE_PARAMETERS = 9;
|
|
2810
|
+
/** The two more a `pr` filter binds (`repo`, `pr_number`), taken from the same headroom only when asked. */
|
|
2811
|
+
const RUN_LIST_PR_PARAMETERS = 2;
|
|
2607
2812
|
|
|
2608
2813
|
/** How many `?` a filter binds (one per id, one per user). */
|
|
2609
2814
|
function boundParameters(f: RunVisibilityFilter): number {
|
|
@@ -3800,13 +4005,13 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
|
|
|
3800
4005
|
);
|
|
3801
4006
|
return json(result ?? { events: null });
|
|
3802
4007
|
}
|
|
3803
|
-
if (pathname === "/runs/usage
|
|
4008
|
+
if (pathname === "/runs/usage") {
|
|
3804
4009
|
const parsed = parseRunUsageQuery(body);
|
|
3805
4010
|
if (!parsed.ok) return json({ error: parsed.error }, 400);
|
|
3806
4011
|
const { storeKey, sinceMs, untilMs } = parsed.value;
|
|
3807
|
-
const
|
|
3808
|
-
console.log(`[runs/usage
|
|
3809
|
-
return json(
|
|
4012
|
+
const rows = await stub(storeKey).usage(sinceMs, untilMs);
|
|
4013
|
+
console.log(`[runs/usage] ${storeKey} -> ${rows.runs.length} runs, ${rows.pending} pending`);
|
|
4014
|
+
return json(rows);
|
|
3810
4015
|
}
|
|
3811
4016
|
// /runs/delete
|
|
3812
4017
|
const parsed = parseRunTarget(body);
|
|
@@ -3831,7 +4036,7 @@ const ROUTES = new Set([
|
|
|
3831
4036
|
"/runs/summary",
|
|
3832
4037
|
"/runs/list",
|
|
3833
4038
|
"/runs/events",
|
|
3834
|
-
"/runs/usage
|
|
4039
|
+
"/runs/usage",
|
|
3835
4040
|
"/runs/delete",
|
|
3836
4041
|
...LEDGER_ROUTES,
|
|
3837
4042
|
]);
|
|
@@ -79,6 +79,10 @@ export interface ReclaimInput {
|
|
|
79
79
|
isDefaultRef: boolean;
|
|
80
80
|
/** Thread exec/read/write currently running on this binding. */
|
|
81
81
|
busy: number;
|
|
82
|
+
/** A run registered on the thread — attached and not yet released (item
|
|
83
|
+
* 44): its process lives in the container between the bot's calls, so
|
|
84
|
+
* `busy` reads 0 while its model thinks. Held is kept, whatever the fate. */
|
|
85
|
+
held: boolean;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
/** The PR that decided the fate (for the audit trail): the open one, else the
|
|
@@ -93,11 +97,13 @@ export type ReclaimWhy =
|
|
|
93
97
|
| "no-pr"
|
|
94
98
|
| "fate-unknown"
|
|
95
99
|
| "busy"
|
|
100
|
+
| "run-held"
|
|
96
101
|
| "re-attached"
|
|
97
102
|
| Extract<RefFate, "gone" | "merged" | "closed">;
|
|
98
103
|
|
|
99
104
|
/** Evict this binding now? A finished ref (gone/merged/closed) is reclaimed
|
|
100
|
-
* when nothing runs on it —
|
|
105
|
+
* when nothing runs on it — no op in flight and no run registered from its
|
|
106
|
+
* attach to its release — and nothing else is asked. What its tree holds
|
|
101
107
|
* is never a reason to keep it: a run starts from a clean tree, so dirt in a
|
|
102
108
|
* tree no run is using protects nothing (the next attach wipes it); the
|
|
103
109
|
* Worker measures and records what it removes instead. Keeps are named so
|
|
@@ -113,6 +119,7 @@ export function reclaimDecision(input: ReclaimInput): { reclaim: boolean; why: R
|
|
|
113
119
|
return { reclaim: false, why: "fate-unknown" };
|
|
114
120
|
}
|
|
115
121
|
if (input.busy > 0) return { reclaim: false, why: "busy" };
|
|
122
|
+
if (input.held) return { reclaim: false, why: "run-held" };
|
|
116
123
|
return { reclaim: true, why: input.fate };
|
|
117
124
|
}
|
|
118
125
|
|