@coreplane/switchboard 1.239.0 → 1.241.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 +32 -7
- package/dist/assets/deploy/cloudflare/worker.ts +77 -18
- package/dist/assets/deploy/cloudflare-memory/worker.ts +115 -1
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +7 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +224 -70
- package/dist/assets/deploy/secrets.manifest.json +7 -1
- package/dist/assets/package-lock.json +207 -3
- package/dist/assets/package.json +2 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +27 -8
- package/dist/assets/src/core/authz/actor.ts +38 -2
- package/dist/assets/src/core/authz/grants.ts +20 -9
- package/dist/assets/src/core/authz/policy.ts +6 -0
- package/dist/assets/src/core/coordinator/contract.ts +49 -1
- package/dist/assets/src/core/coordinator/driver.ts +30 -1
- package/dist/assets/src/core/coordinator/instancesRoute.ts +72 -0
- package/dist/assets/src/core/costs.ts +1119 -0
- package/dist/assets/src/core/costsSnapshotStore.ts +209 -0
- package/dist/assets/src/core/delivery.ts +4 -11
- package/dist/assets/src/core/deliverySnapshotStore.ts +12 -28
- package/dist/assets/src/core/ingressTokens.ts +27 -13
- package/dist/assets/src/core/runLedger/types.ts +3 -0
- package/dist/assets/src/core/runRecord.ts +10 -3
- package/dist/assets/src/core/schedules.ts +2 -1
- package/dist/assets/src/core/ship/coordinator.ts +92 -7
- package/dist/assets/src/core/snapshotAge.ts +30 -0
- package/dist/assets/src/core/stateWorkerRef.ts +41 -0
- package/dist/assets/src/core/trace/attrs.ts +1 -1
- package/dist/assets/src/execution/bindingPurge.ts +2 -2
- package/dist/assets/src/execution/residentAutoRebuild.ts +73 -0
- package/dist/assets/src/execution/residentRefresh.ts +25 -3
- package/dist/assets/src/execution/restoreWaiters.ts +61 -0
- package/dist/assets/src/mcp/registry.ts +7 -0
- package/dist/assets/web/dist/.vite/manifest.json +509 -427
- package/dist/assets/web/dist/assets/AppShell-DEy5jRuS.js +1 -0
- package/dist/assets/web/dist/assets/CostsPage-BuHD-Bv7.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-BGpPr5xr.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +2 -0
- package/dist/assets/web/dist/assets/NotFoundPage-DtmQM2gD.js +1 -0
- package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CrfCd8fx.js → ResidentDetailPage-C-48fxat.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CBBIjPOa.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-XpCqeWu4.js → RunsTabs-CtNGNnhN.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-Ci1Ygqnc.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +1 -0
- package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-DkXALvKf.js → StatusDot-DPWE1JmO.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DtLIyi5P.js → Tooltip-DKhSRH9t.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DEN5bSQf.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-DYtV9uIN.js → angular-html-DgSK1qvr.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DF2P25Zp.js → angular-ts-D3gNdiSG.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-Ff96wxko.js → apl-CkHCYM8I.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-LbBnZna-.js → astro-DLm45axt.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-DltuxeBW.js → blade-BKa-VE-c.js} +1 -1
- package/dist/assets/web/dist/assets/{c-DBg7yuPx.js → c-C8NCNWai.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-C83hUAds.js → chapel-DqDQ7IKH.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DB0yV_2D.js → cobol-j4vCD6AJ.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-Bpih6Kzl.js → coffee-C6oDVH-0.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CU00ZeRn.js → cpp-Bd3A1Rtc.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-f8ImJkux.js → crystal-CUOgOw0d.js} +1 -1
- package/dist/assets/web/dist/assets/{css-4VrC4ISo.js → css-RYljyv7G.js} +1 -1
- package/dist/assets/web/dist/assets/delivery-CTdOqpVK.js +1 -0
- package/dist/assets/web/dist/assets/{dist-DtA7JiH5.js → dist-BnJXKHCY.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BwFDzfS_.js → edge-BJhNdQDc.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-wwIYIpJm.js → elixir-g1AWOAfT.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DyDuM3v3.js → elm-CER5e4Dz.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-DFJBEVrh.js → erb-D5aOmMQf.js} +1 -1
- package/dist/assets/web/dist/assets/eventSource-tRprhqm7.js +1 -0
- package/dist/assets/web/dist/assets/favicon-D1hkyC5j.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-CuDv-44X.js → git-rebase-D6rfV8jp.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-ZFnrm4J3.js → glimmer-js-BvDH3-mC.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-XY4Yqshp.js → glimmer-ts-DChtNs9V.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-xnR8efx0.js → glsl-BHlsWrxf.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-D-gO5tnP.js → graphql-BsaBHb53.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-CSxbW4ax.js → hack-0lbDosCd.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-D4ar42QT.js → haml-BKtco2vr.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-CMLbDE7o.js → handlebars-BE_cj04z.js} +1 -1
- package/dist/assets/web/dist/assets/{html-CVekU4Fs.js → html-BNcr8EVd.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-St3HHw_N.js → html-derivative-BsJt2Kei.js} +1 -1
- package/dist/assets/web/dist/assets/{http-Mon0DWHS.js → http-BR5P8ER_.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-Cbl5IvBs.js → hurl-CtHalpWS.js} +1 -1
- package/dist/assets/web/dist/assets/{localIso-CNA-bhS-.js → indexFormat-B-pd4r7Y.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-FLJu5mR9.js → indexRow-BD1VT8o8.js} +1 -1
- package/dist/assets/web/dist/assets/{java-Bprck1l-.js → java-BXVdhN11.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-C73G1-UR.js → javascript-CyoShgNQ.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-C4DuRxYJ.js → jinja-BQYZzHex.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-B2U03GEC.js → jison-DqrUa1Eq.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CvTZUDzx.js → json-CQiTF9Jj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-DMWKCzwo.js → jsx-Y16MkCYQ.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-Czn7bV_3.js → julia-zn16YONe.js} +1 -1
- package/dist/assets/web/dist/assets/{just-BBxpNfvC.js → just-DcVeN_MN.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-Bw9yg5mr.js → latex-BNNoF-WP.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BRCjaydE.js → liquid-DAi6qHzn.js} +1 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +1 -0
- package/dist/assets/web/dist/assets/{lua-DUu2KIC0.js → lua-BswS0axw.js} +1 -1
- package/dist/assets/web/dist/assets/main-B3is4sk6.js +28 -0
- package/dist/assets/web/dist/assets/main-DP_zSemY.css +1 -0
- package/dist/assets/web/dist/assets/{marko-CZJZIlSQ.js → marko-C-4hkmQZ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-BmClq6fd.js → mdc-0ZaHUkS7.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Bmt3xSEm.js → nginx-DBDg5tOR.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-Da-i0utK.js → nim-BVMz49Yw.js} +1 -1
- package/dist/assets/web/dist/assets/{org-BvAre2qP.js → org-1hO_K6ya.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-v9WwFFtB.js → perl-CVRQLvVk.js} +1 -1
- package/dist/assets/web/dist/assets/{php-DTU4Zyze.js → php-K9nCrB9n.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-DhTwEqTO.js → pug-CTxOcmO3.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BUD9AC_c.js → qml-CmCkfG5q.js} +1 -1
- package/dist/assets/web/dist/assets/{r-CAnu56n7.js → r-pP47Xn1X.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-Dn0P4gAq.js → razor-BpW6r3tC.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CZyBRaVW.js → regexp-CINgdY4N.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-Ckyd5UsY.js → rst-BZIUEq2Q.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-BBQ62fsx.js → ruby-nzAOxz6r.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-CW35hMxG.js → sas-QIS1bFth.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-CpV8pF4J.js → scss-BuhBBVNt.js} +1 -1
- package/dist/assets/web/dist/assets/settingsApi-priOPiwu.js +1 -0
- package/dist/assets/web/dist/assets/{shellscript-CRJvsmE1.js → shellscript-CQc1vXbk.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CyriUZxf.js → shellsession-CoubCAUv.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-D5QzYUeg.js → soy-CPRzlder.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-CUBf_Mse.js → sql-W9krb8-9.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-yji8a1wM.js +9 -0
- package/dist/assets/web/dist/assets/{stata-B-4n4ITz.js → stata-CuISJEC0.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-RuWZBLtr.js → surrealql-CSet7584.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-CO5YzCsK.js → svelte-ChXTSYwK.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DbkWVaBF.js → templ-Bm55v62k.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-IuxoEcId.js → tex-CICMX8Gj.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-g6dGQPjd.js → ts-tags-CA1UzWyB.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-WnXwiyH2.js → tsx-Dy04HNbv.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-Lx1G7PnY.js → twig-BBHsVnVD.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BQd7biLF.js → typescript-Dvc-wVBT.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-D2lXPHK7.js → typst-Dvlqx3_q.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DZPaRte4.js → vue-html-D4YxT3An.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-DbmZCmu-.js → vue-vine-0YE7uQlH.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-4wVQD9y9.js → vue-zWi1MNU2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-DV16szyo.js → xml-gm-iksZB.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-eo5-1sk0.js → xsl-D_W5BcrY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BXdX8HZf.js → yaml-CHmZ21wZ.js} +1 -1
- package/dist/cli.js +6014 -2362
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-MYHNxJks.js +0 -1
- package/dist/assets/web/dist/assets/CostsPage-DKk9OjB0.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-Kz6js49i.js +0 -1
- package/dist/assets/web/dist/assets/NotFoundPage-BXLrQQzy.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-BdW0k2h3.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CbTd9SiS.js +0 -9
- package/dist/assets/web/dist/assets/RunRoutePage-br0H-8Pz.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-Bnzb15zH.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-NLhjzHAz.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-D-i0uVw-.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DGXukEAH.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-CYMux25J.js +0 -1
- package/dist/assets/web/dist/assets/favicon-CSt-qDvc.js +0 -1
- package/dist/assets/web/dist/assets/main-CFHF2RvQ.css +0 -1
- package/dist/assets/web/dist/assets/main-Dht0in4v.js +0 -28
|
@@ -323,14 +323,24 @@ workspaceDir: ./workspaces
|
|
|
323
323
|
# ship:
|
|
324
324
|
# maxRounds: 3 # review rounds per unit
|
|
325
325
|
# maxMinutes: 120 # the ship preset's wall-clock budget in minutes (the registry's default)
|
|
326
|
+
# addressSeverity: minor # the severity an approve's findings are held to (blocking|major|minor|nit);
|
|
327
|
+
# # a finding at or above it sends the round into a fix step instead of merge-ready.
|
|
328
|
+
# # Per channel/user: config set … --ship.addressSeverity; per run: severity:<level>.
|
|
326
329
|
|
|
327
|
-
#
|
|
328
|
-
#
|
|
329
|
-
# coding agent
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
330
|
+
# Which harness each preset's runs are driven by (docs/reference/specs/harness.md
|
|
331
|
+
# item 8): a mapping of preset to a harness's name. Two exist — `pi` (the pi
|
|
332
|
+
# coding agent, the default for every preset) and `opencode` (OpenCode's server,
|
|
333
|
+
# `opencode serve`) — each started inside the run's own execution container (or
|
|
334
|
+
# as a child of the bot for a preset without a workspace) with the run's
|
|
335
|
+
# model-proxy bearer as its only key, every tool call decided in the bot, every
|
|
336
|
+
# event on the run's record. A preset the block does not name runs on pi;
|
|
337
|
+
# nothing defaults to OpenCode. A run keeps the harness it started on, so
|
|
338
|
+
# changing a word moves the next run, never one in flight. Any other word
|
|
339
|
+
# (`native`, the deleted loop; `codex`) fails the load naming the two, as does
|
|
340
|
+
# an unknown preset. How-to: docs/how-to/put-a-preset-on-opencode.md.
|
|
341
|
+
# harness:
|
|
342
|
+
# coding: opencode # coding runs on OpenCode
|
|
343
|
+
# review: pi # the default, written out
|
|
334
344
|
|
|
335
345
|
# What the harness writes into pi's per-run settings for every run on pi
|
|
336
346
|
# (docs/reference/specs/harness-pi.md item 4): pi's compaction thresholds, in
|
|
@@ -346,6 +356,18 @@ workspaceDir: ./workspaces
|
|
|
346
356
|
# reserveTokens: 150000
|
|
347
357
|
# keepRecentTokens: 8000
|
|
348
358
|
|
|
359
|
+
# What the harness writes into OpenCode's per-run configuration for every run
|
|
360
|
+
# on OpenCode (docs/reference/specs/harness.md item 8): OpenCode's compaction
|
|
361
|
+
# thresholds, in tokens, under OpenCode's own names — `buffer`, the tokens kept
|
|
362
|
+
# free of the model's window before a compaction runs; `keepTokens`, how much
|
|
363
|
+
# of the newest turns it keeps. Unset, OpenCode's defaults stand and the file
|
|
364
|
+
# names no compaction. The OpenCode twin of the `pi` block above; positive
|
|
365
|
+
# integers only, any other shape or key fails the load by name.
|
|
366
|
+
# opencode:
|
|
367
|
+
# compaction:
|
|
368
|
+
# buffer: 20000
|
|
369
|
+
# keepTokens: 8000
|
|
370
|
+
|
|
349
371
|
# The fan-out cap a spawning run meets (docs/reference/specs/agent-conductor.md).
|
|
350
372
|
# `agent:conductor` starts child runs as the person who asked — each an
|
|
351
373
|
# ordinary run in a thread of its own, under that person's permissions — and
|
|
@@ -386,6 +408,9 @@ workspaceDir: ./workspaces
|
|
|
386
408
|
# cloudflareAccountName: acme-infra # optional: how the page names the account beside its share (else the id's first hex)
|
|
387
409
|
# cloudflareTokenEnv: CF_ANALYTICS_TOKEN # token with Account Analytics:Read
|
|
388
410
|
# anthropicAdminKeyEnv: ANTHROPIC_ADMIN_KEY # optional: sk-ant-admin… for LLM spend
|
|
411
|
+
# snapshot: # the page serves a snapshot of both billing sources, never a live read;
|
|
412
|
+
# everyHours: 24 # how often the bot reads them (1–168, default 24). `costs snapshot`
|
|
413
|
+
# # takes one now. Kept on the state Worker any `*.worker` block names
|
|
389
414
|
# groups:
|
|
390
415
|
# myapp:
|
|
391
416
|
# label: My App
|
|
@@ -17,8 +17,11 @@ import {
|
|
|
17
17
|
createInstanceResponse,
|
|
18
18
|
instanceStatusResponse,
|
|
19
19
|
isInstanceNotFound,
|
|
20
|
+
parseInstanceEventPath,
|
|
20
21
|
parseInstanceStatusPath,
|
|
21
22
|
parseCreateInstanceRequest,
|
|
23
|
+
parseSendEventRequest,
|
|
24
|
+
sendEventResponse,
|
|
22
25
|
parseSubjectAuthorization,
|
|
23
26
|
type CreateInstanceOutcome,
|
|
24
27
|
} from "../../src/core/coordinator/instancesRoute.ts";
|
|
@@ -87,6 +90,7 @@ export interface Env {
|
|
|
87
90
|
DASHBOARD_TOKEN?: string; // dashboard auth `token` strategy: the bearer (the default env name; config may name another)
|
|
88
91
|
SWITCHBOARD_INGRESS_TOKENS?: string; // enables HTTP /ingress + MCP /mcp (JSON token→identity map); the `cron` entry is what scheduled runs present; an entry whose `http:<subject>` actor holds `deploy:write` in the bot's config may POST /admin/restart
|
|
89
92
|
BRAVE_SEARCH_API_KEY?: string; // web_search backend (Brave); web_fetch works without it
|
|
93
|
+
GITHUB_WEBHOOK_SECRET?: string; // check-run intake: signs POST /webhooks/github; absent, the intake answers 503 disabled
|
|
90
94
|
CF_ANALYTICS_TOKEN?: string; // costs dash: Cloudflare API token, Account Analytics:Read only
|
|
91
95
|
ANTHROPIC_ADMIN_KEY?: string; // costs dash (optional): Anthropic Admin API key for the LLM cost report
|
|
92
96
|
MEMORY_TOKEN?: string; // durable memory + friction ledger + schedule firings + MCP registry: bearer for the state Worker
|
|
@@ -125,6 +129,7 @@ const FORWARDED_OPTIONAL = [
|
|
|
125
129
|
"ANTHROPIC_ADMIN_KEY",
|
|
126
130
|
"SWITCHBOARD_INGRESS_TOKENS",
|
|
127
131
|
"BRAVE_SEARCH_API_KEY",
|
|
132
|
+
"GITHUB_WEBHOOK_SECRET",
|
|
128
133
|
"MEMORY_TOKEN",
|
|
129
134
|
"MCP_CREDENTIAL_KEY",
|
|
130
135
|
"MCP_ACCESS_CLIENT_ID",
|
|
@@ -407,6 +412,57 @@ async function handleCoordinatorInstanceStatus(request: Request, env: Env, id: s
|
|
|
407
412
|
return json(res.status, res.body);
|
|
408
413
|
}
|
|
409
414
|
|
|
415
|
+
/** `POST /admin/coordinator/instances/<id>/events` — the shim's event relay
|
|
416
|
+
* (docs/reference/specs/http-ingress.md item 12): the bot's check-run intake
|
|
417
|
+
* holds no Workflow binding, so its `checks-settled-<head>` send crosses here
|
|
418
|
+
* and this Worker's own `SHIP_COORDINATOR` binding delivers it. The same door
|
|
419
|
+
* as the create and the status: the bearer in the map, the bot's `authorize`
|
|
420
|
+
* answer. The engine's `instance.not_found` is 404; any other refusal (an
|
|
421
|
+
* instance that already ended) is 502 by reason — the sender treats both as a
|
|
422
|
+
* failed send and the driver's bounded wait is the fallback. */
|
|
423
|
+
async function handleCoordinatorInstanceEvent(request: Request, env: Env, id: string): Promise<Response> {
|
|
424
|
+
const json = (status: number, body: Record<string, unknown>) =>
|
|
425
|
+
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
426
|
+
if (request.method !== "POST")
|
|
427
|
+
return json(405, {
|
|
428
|
+
ok: false,
|
|
429
|
+
error: `method not allowed: ${request.method} — ${COORDINATOR_INSTANCES_PATH}/<id>/events answers POST only`,
|
|
430
|
+
});
|
|
431
|
+
const authorization = request.headers.get("authorization") ?? undefined;
|
|
432
|
+
const authn = authenticateIngressBearer(authorization, env.SWITCHBOARD_INGRESS_TOKENS, "coordinator");
|
|
433
|
+
if (!authn.ok) {
|
|
434
|
+
console.warn(`[coordinator] instance event ${authn.status} — ${authn.reason}`);
|
|
435
|
+
return json(authn.status, { ok: false, error: authn.reason });
|
|
436
|
+
}
|
|
437
|
+
let answer: Response;
|
|
438
|
+
try {
|
|
439
|
+
answer = await getContainer(env.SWITCHBOARD, INSTANCE).fetch(
|
|
440
|
+
new Request(`${INTERNAL}${COORDINATOR_AUTHORIZE_PATH}`, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: { authorization: authorization ?? "" },
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
447
|
+
return json(503, { ok: false, error: `coordinator disabled: the bot could not be asked (${reason})` });
|
|
448
|
+
}
|
|
449
|
+
const auth = parseSubjectAuthorization(answer.status, await answer.text().catch(() => ""));
|
|
450
|
+
if (!auth.ok) return json(auth.status, { ok: false, error: auth.reason });
|
|
451
|
+
const parsed = parseSendEventRequest(await request.text().catch(() => ""));
|
|
452
|
+
if (!parsed.ok) return json(400, { ok: false, error: parsed.reason });
|
|
453
|
+
let outcome: Parameters<typeof sendEventResponse>[0];
|
|
454
|
+
try {
|
|
455
|
+
await (await env.SHIP_COORDINATOR.get(id)).sendEvent({ type: parsed.type, payload: parsed.payload });
|
|
456
|
+
outcome = { kind: "sent", id };
|
|
457
|
+
} catch (err) {
|
|
458
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
459
|
+
outcome = isInstanceNotFound(reason) ? { kind: "absent", id } : { kind: "failed", id, reason };
|
|
460
|
+
}
|
|
461
|
+
const res = sendEventResponse(outcome);
|
|
462
|
+
console.log(`[coordinator] ${auth.subject} → instance ${id} event ${parsed.type}: ${outcome.kind}`);
|
|
463
|
+
return json(res.status, res.body);
|
|
464
|
+
}
|
|
465
|
+
|
|
410
466
|
/** Record a firing on the state Worker's ScheduleDO (the /runs Scheduled panel
|
|
411
467
|
* reads it). Best-effort: a failure here is a log line — the run itself (if
|
|
412
468
|
* any) already happened and is its own record. */
|
|
@@ -440,29 +496,32 @@ export default {
|
|
|
440
496
|
// The routes the Worker answers itself — the restart, the coordinator's
|
|
441
497
|
// instance creation and an instance's status, all over bindings only this
|
|
442
498
|
// Worker holds; everything else is the container's.
|
|
443
|
-
const
|
|
499
|
+
const eventsId = parseInstanceEventPath(pathname);
|
|
500
|
+
const statusId = eventsId === undefined ? parseInstanceStatusPath(pathname) : undefined;
|
|
444
501
|
const res =
|
|
445
502
|
pathname === "/admin/restart"
|
|
446
503
|
? await handleAdminRestart(forwarded, env)
|
|
447
504
|
: pathname === COORDINATOR_INSTANCES_PATH
|
|
448
505
|
? await handleCoordinatorInstances(forwarded, env)
|
|
449
|
-
:
|
|
450
|
-
? await
|
|
451
|
-
:
|
|
452
|
-
?
|
|
453
|
-
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
506
|
+
: eventsId !== undefined
|
|
507
|
+
? await handleCoordinatorInstanceEvent(forwarded, env, eventsId)
|
|
508
|
+
: statusId !== undefined
|
|
509
|
+
? await handleCoordinatorInstanceStatus(forwarded, env, statusId)
|
|
510
|
+
: pathname === COPY_PATH
|
|
511
|
+
? // The artifact copy (artifactsCopy.ts): the R2 binding and the Slack
|
|
512
|
+
// token are this Worker's; the bot only asks, with its copy bearer.
|
|
513
|
+
// `forwarded`, never `inbound`: `withTraceContext` rebuilt the request
|
|
514
|
+
// with `new Request(inbound, …)`, which takes the body stream with it —
|
|
515
|
+
// `inbound.text()` is empty afterwards and the route read "not JSON" live.
|
|
516
|
+
await handleArtifactsCopy(forwarded, {
|
|
517
|
+
bucket: env.ARTIFACTS,
|
|
518
|
+
bucketName: env.ARTIFACTS_BUCKET_NAME,
|
|
519
|
+
copyToken: env.ARTIFACTS_COPY_TOKEN,
|
|
520
|
+
slackToken: env.SLACK_BOT_TOKEN,
|
|
521
|
+
fetch: (input, init) => fetch(input, init),
|
|
522
|
+
lengthPipe: (size) => new FixedLengthStream(size),
|
|
523
|
+
})
|
|
524
|
+
: withLength(await getContainer(env.SWITCHBOARD, INSTANCE).fetch(forwarded));
|
|
466
525
|
root.end(res.status >= 500 ? "error" : "ok", { httpStatus: res.status });
|
|
467
526
|
return res;
|
|
468
527
|
} catch (err) {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../../src/core/memory/engine.ts";
|
|
11
11
|
import { tokenize } from "../../src/core/memory/scorer.ts";
|
|
12
12
|
import { FIRING_DETAIL_MAX, isScheduleFiring, type ScheduleFiring } from "../../src/core/schedules.ts";
|
|
13
|
+
import { isCostsSnapshot, type CostsSnapshot } from "../../src/core/costsSnapshotStore.ts";
|
|
13
14
|
import { REPO_SLUG } from "../../src/core/delivery.ts";
|
|
14
15
|
import {
|
|
15
16
|
isDeliverySnapshot,
|
|
@@ -176,6 +177,8 @@ export interface Env {
|
|
|
176
177
|
SESSION_LOGS: DurableObjectNamespace<SessionLogDO>;
|
|
177
178
|
/** Delivery snapshots (delivery item 10): ONE DeliveryDO (named "delivery"), one snapshot per repository. */
|
|
178
179
|
DELIVERY: DurableObjectNamespace<DeliveryDO>;
|
|
180
|
+
/** The costs snapshot (costs item 6): ONE CostsSnapshotDO (named "costs"), one snapshot per installation. */
|
|
181
|
+
COSTS: DurableObjectNamespace<CostsSnapshotDO>;
|
|
179
182
|
/** The ship coordinator Workflow in the bot's shim Worker (run-history item
|
|
180
183
|
* 47): where `RunHistoryDO.finish` sends `run-finished-<runId>` for a record
|
|
181
184
|
* carrying `parentInstanceId`. Optional: this Worker deploys without it (the
|
|
@@ -188,6 +191,9 @@ export interface Env {
|
|
|
188
191
|
/** The single DeliveryDO's name — every repository's snapshot lives in one object. */
|
|
189
192
|
const DELIVERY_OBJECT = "delivery";
|
|
190
193
|
|
|
194
|
+
/** The single CostsSnapshotDO's name — the installation has one costs snapshot. */
|
|
195
|
+
const COSTS_OBJECT = "costs";
|
|
196
|
+
|
|
191
197
|
/** The single ConfigDO's name. */
|
|
192
198
|
const CONFIG_OBJECT = "config";
|
|
193
199
|
/** A config document key: short, lowercase, like `overrides`. */
|
|
@@ -929,6 +935,110 @@ async function handleDelivery(pathname: string, body: unknown, env: Env): Promis
|
|
|
929
935
|
return json({ error: "not found" }, 404);
|
|
930
936
|
}
|
|
931
937
|
|
|
938
|
+
// ---------------------------------------------------------------------------
|
|
939
|
+
// The costs snapshot (docs/reference/specs/costs.md item 6)
|
|
940
|
+
// ---------------------------------------------------------------------------
|
|
941
|
+
|
|
942
|
+
/** The datasets a snapshot's `usage` carries, each stored as its own row. */
|
|
943
|
+
const USAGE_PARTS = [
|
|
944
|
+
"containers",
|
|
945
|
+
"durableObjectRequests",
|
|
946
|
+
"durableObjectDays",
|
|
947
|
+
"durableObjectStorage",
|
|
948
|
+
"workers",
|
|
949
|
+
"r2Storage",
|
|
950
|
+
"r2Operations",
|
|
951
|
+
"workflows",
|
|
952
|
+
] as const;
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* The installation's one costs snapshot: both billing sources' rows over the
|
|
956
|
+
* page's widest range plus the run history's per-user usage, and when they
|
|
957
|
+
* were read (`CostsSnapshot`, the shape the bot validates too). Stored as one
|
|
958
|
+
* row per part — the meta, each usage dataset, the LLM rows, the run usage —
|
|
959
|
+
* so no single value nears the row limit as the window's rows grow; replaced
|
|
960
|
+
* whole on every put, in one transaction, and read back as one document.
|
|
961
|
+
*/
|
|
962
|
+
export class CostsSnapshotDO extends DurableObject<Env> {
|
|
963
|
+
private readonly sql: SqlStorage;
|
|
964
|
+
|
|
965
|
+
constructor(ctx: DurableObjectState, env: Env) {
|
|
966
|
+
super(ctx, env);
|
|
967
|
+
this.sql = ctx.storage.sql;
|
|
968
|
+
this.sql.exec(`
|
|
969
|
+
CREATE TABLE IF NOT EXISTS parts (
|
|
970
|
+
part TEXT PRIMARY KEY,
|
|
971
|
+
body TEXT NOT NULL
|
|
972
|
+
);
|
|
973
|
+
`);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Replace the snapshot whole: every part rewritten in one transaction. */
|
|
977
|
+
async put(snapshot: CostsSnapshot): Promise<void> {
|
|
978
|
+
const { usage, llm, runUsage, ...meta } = snapshot;
|
|
979
|
+
const rows: Array<[string, unknown]> = [
|
|
980
|
+
["meta", meta],
|
|
981
|
+
...USAGE_PARTS.map((name): [string, unknown] => [`usage.${name}`, usage[name]]),
|
|
982
|
+
["llm", llm],
|
|
983
|
+
["runUsage", runUsage],
|
|
984
|
+
];
|
|
985
|
+
this.ctx.storage.transactionSync(() => {
|
|
986
|
+
this.sql.exec(`DELETE FROM parts`);
|
|
987
|
+
for (const [part, body] of rows)
|
|
988
|
+
this.sql.exec(`INSERT INTO parts (part, body) VALUES (?, ?)`, part, JSON.stringify(body));
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** The stored snapshot reassembled from its parts, or null when none was stored (or the parts do not make one). */
|
|
993
|
+
async get(): Promise<CostsSnapshot | null> {
|
|
994
|
+
const parts = new Map(
|
|
995
|
+
this.sql
|
|
996
|
+
.exec<{ part: string; body: string }>(`SELECT part, body FROM parts`)
|
|
997
|
+
.toArray()
|
|
998
|
+
.map((r) => [r.part, r.body]),
|
|
999
|
+
);
|
|
1000
|
+
const meta = parts.get("meta");
|
|
1001
|
+
if (meta === undefined) return null;
|
|
1002
|
+
const read = (part: string): unknown => {
|
|
1003
|
+
const body = parts.get(part);
|
|
1004
|
+
return body === undefined ? undefined : (JSON.parse(body) as unknown);
|
|
1005
|
+
};
|
|
1006
|
+
const usage = Object.fromEntries(USAGE_PARTS.map((name) => [name, read(`usage.${name}`)]));
|
|
1007
|
+
const snapshot = {
|
|
1008
|
+
...(JSON.parse(meta) as Record<string, unknown>),
|
|
1009
|
+
usage,
|
|
1010
|
+
llm: read("llm"),
|
|
1011
|
+
runUsage: read("runUsage"),
|
|
1012
|
+
};
|
|
1013
|
+
return isCostsSnapshot(snapshot) ? snapshot : null;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const COSTS_ROUTES = new Set(["/costs/snapshot/get", "/costs/snapshot/put"]);
|
|
1018
|
+
|
|
1019
|
+
async function handleCosts(pathname: string, body: unknown, env: Env): Promise<Response> {
|
|
1020
|
+
const b = (typeof body === "object" && body !== null ? body : {}) as Record<string, unknown>;
|
|
1021
|
+
const dO = env.COSTS.get(env.COSTS.idFromName(COSTS_OBJECT));
|
|
1022
|
+
if (pathname === "/costs/snapshot/get") {
|
|
1023
|
+
const snapshot = await dO.get();
|
|
1024
|
+
console.log(
|
|
1025
|
+
`[costs/snapshot/get] -> ${snapshot ? `snapshot taken ${snapshot.takenAt} by ${snapshot.takenBy}` : "none"}`,
|
|
1026
|
+
);
|
|
1027
|
+
return json({ snapshot });
|
|
1028
|
+
}
|
|
1029
|
+
if (pathname === "/costs/snapshot/put") {
|
|
1030
|
+
if (!isCostsSnapshot(b.snapshot))
|
|
1031
|
+
return json(
|
|
1032
|
+
{ error: "snapshot must be a CostsSnapshot (takenAt, takenBy, durationMs, range, usage, llm, runUsage)" },
|
|
1033
|
+
400,
|
|
1034
|
+
);
|
|
1035
|
+
await dO.put(b.snapshot);
|
|
1036
|
+
console.log(`[costs/snapshot/put] <- snapshot taken ${b.snapshot.takenAt} by ${b.snapshot.takenBy}`);
|
|
1037
|
+
return json({ ok: true });
|
|
1038
|
+
}
|
|
1039
|
+
return json({ error: "not found" }, 404);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
932
1042
|
/** Documents are small; a body over this is refused before storage. */
|
|
933
1043
|
const MAX_CONFIG_DOCUMENT_BYTES = 256 * 1024;
|
|
934
1044
|
|
|
@@ -3229,6 +3339,8 @@ const MAX_SNAPSHOT_BODY_BYTES = 16 * 1024 * 1024;
|
|
|
3229
3339
|
function bodyFenceFor(pathname: string): number {
|
|
3230
3340
|
if (WIDE_BODY_ROUTES.has(pathname)) return MAX_RUN_PUT_BODY_BYTES;
|
|
3231
3341
|
if (pathname === "/delivery/put" || pathname === "/delivery/merge") return MAX_SNAPSHOT_BODY_BYTES;
|
|
3342
|
+
// A ninety-day costs snapshot is a few hundred KB today and grows with the account's rows.
|
|
3343
|
+
if (pathname === "/costs/snapshot/put") return MAX_SNAPSHOT_BODY_BYTES;
|
|
3232
3344
|
return MAX_BODY_BYTES;
|
|
3233
3345
|
}
|
|
3234
3346
|
|
|
@@ -3707,6 +3819,7 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
|
|
|
3707
3819
|
const ROUTES = new Set([
|
|
3708
3820
|
...CONFIG_ROUTES,
|
|
3709
3821
|
...DELIVERY_ROUTES,
|
|
3822
|
+
...COSTS_ROUTES,
|
|
3710
3823
|
"/retrieve",
|
|
3711
3824
|
"/write",
|
|
3712
3825
|
"/list",
|
|
@@ -3735,7 +3848,7 @@ interface Admission {
|
|
|
3735
3848
|
async function handleRequest(request: Request, env: Env, admission: Admission): Promise<Response> {
|
|
3736
3849
|
const url = new URL(request.url);
|
|
3737
3850
|
if (url.pathname === "/healthz" && request.method === "GET")
|
|
3738
|
-
return json({ ok: true, build: BUILD, features: ["memory", "schedules", "runs", "config", "delivery"] });
|
|
3851
|
+
return json({ ok: true, build: BUILD, features: ["memory", "schedules", "runs", "config", "delivery", "costs"] });
|
|
3739
3852
|
if (!admission.known) return json({ error: "not found" }, 404);
|
|
3740
3853
|
if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
3741
3854
|
if (!admission.authorized) return json({ error: "unauthorized" }, 401);
|
|
@@ -3787,6 +3900,7 @@ async function handleRequest(request: Request, env: Env, admission: Admission):
|
|
|
3787
3900
|
if (url.pathname.startsWith("/runs/")) return handleRuns(url.pathname, body, env);
|
|
3788
3901
|
if (url.pathname.startsWith("/config/")) return handleConfig(url.pathname, body, env);
|
|
3789
3902
|
if (url.pathname.startsWith("/delivery/")) return handleDelivery(url.pathname, body, env);
|
|
3903
|
+
if (url.pathname.startsWith("/costs/")) return handleCosts(url.pathname, body, env);
|
|
3790
3904
|
|
|
3791
3905
|
if (url.pathname === "/retrieve") {
|
|
3792
3906
|
const parsed = parseRetrieve(body);
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
// repository — the merged pull requests' facts over the window, one row
|
|
37
37
|
// each, and when they were read (docs/reference/specs/delivery.md item 10).
|
|
38
38
|
{ "name": "DELIVERY", "class_name": "DeliveryDO" },
|
|
39
|
+
// ONE CostsSnapshotDO (named "costs"): the installation's costs snapshot —
|
|
40
|
+
// both billing sources' rows over the costs page's widest range and the run
|
|
41
|
+
// history's per-user usage, one row per part, replaced whole on every take
|
|
42
|
+
// (docs/reference/specs/costs.md item 6).
|
|
43
|
+
{ "name": "COSTS", "class_name": "CostsSnapshotDO" },
|
|
39
44
|
// One SessionLogDO per session — a thread and an agent (the DO name is
|
|
40
45
|
// `<threadKey>:<agent>`): the transcript rows of every run of the
|
|
41
46
|
// session, a full-text index over them, and the notepad
|
|
@@ -74,6 +79,7 @@
|
|
|
74
79
|
// retired with its rows — docs/reference/specs/self-improvement.md item 1.
|
|
75
80
|
{ "tag": "v7", "deleted_classes": ["FrictionDO"] },
|
|
76
81
|
{ "tag": "v8", "new_sqlite_classes": ["DeliveryDO"] },
|
|
77
|
-
{ "tag": "v9", "new_sqlite_classes": ["SessionLogDO"] }
|
|
82
|
+
{ "tag": "v9", "new_sqlite_classes": ["SessionLogDO"] },
|
|
83
|
+
{ "tag": "v10", "new_sqlite_classes": ["CostsSnapshotDO"] }
|
|
78
84
|
]
|
|
79
85
|
}
|