@coreplane/switchboard 1.253.0 → 1.254.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 +40 -11
- package/dist/assets/deploy/cloudflare-memory/worker.ts +310 -7
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/authz/grants.ts +6 -0
- package/dist/assets/src/core/authz/policy.ts +11 -0
- package/dist/assets/src/core/budgets.ts +8 -0
- package/dist/assets/src/core/coordinator/driver.ts +9 -0
- package/dist/assets/src/core/costs.ts +221 -2
- package/dist/assets/src/core/costsSnapshotStore.ts +22 -1
- package/dist/assets/src/core/pipelineStanding.ts +294 -0
- package/dist/assets/src/core/plane/decide.ts +203 -0
- package/dist/assets/src/core/provider.ts +11 -0
- package/dist/assets/src/core/runEvents.ts +26 -0
- package/dist/assets/src/core/runLedger/transcript.ts +23 -3
- package/dist/assets/src/core/runRecord.ts +15 -0
- package/dist/assets/src/core/runUsage.ts +41 -9
- package/dist/assets/src/core/ship/coordinator.ts +45 -9
- package/dist/assets/web/dist/.vite/manifest.json +450 -430
- package/dist/assets/web/dist/assets/{AppShell-Bmz8nzz3.js → AppShell-CSXDCV4x.js} +1 -1
- package/dist/assets/web/dist/assets/CostChart-DsuPS2g7.js +2 -0
- package/dist/assets/web/dist/assets/CostsPage-BuKjw3nv.js +1 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-C7jaEJip.js → DeliveryPage-ngPsO2to.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-cWbupQzy.js → HomePage-DvxTHzPx.js} +3 -3
- package/dist/assets/web/dist/assets/{InputMenu-C9E7YoII.js → InputMenu-BdZXnibG.js} +1 -1
- package/dist/assets/web/dist/assets/MetricsPage-o5fSaCTe.js +1 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-4nlrq7DP.js → NotFoundPage-DHuWyjlq.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +1 -0
- package/dist/assets/web/dist/assets/{PlanePage-BNy4tSgJ.js → PlanePage-DpWfiX4C.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Dbtiq6_J.js → ResidentDetailPage-DG86v39Y.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-CKtz9H1o.js → ResidentsIndexPage-x6p689VH.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-Clh68nw9.js → RunRoutePage-ysJBY8xQ.js} +4 -4
- package/dist/assets/web/dist/assets/RunsIndexPage-CuzFchcn.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-DotpuCSP.js → RunsTabs-kHbbxVHx.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-C8FYoZy5.js → ScheduledPage-BuLmfcbG.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingSelect-CSiBQCSX.js → SettingSelect-BfID5nF4.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-CkYIhLUl.js → SettingsPage-BujWkdU_.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-C0TUgnC1.js → StatusDot-C8Bc0pTX.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CN5cZdl3.js → Tooltip-BWwJx27K.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-D_EtAl8r.js → UnitRoutePage-B9kjA1AT.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-qW40ZL9x.js → angular-html-C8b5YTHf.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-PUtZU8Cq.js → angular-ts-C3f4WYNe.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-D3fC7Qjd.js → apl-DSHxUJc3.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-Bi3JFHhh.js → astro-BSGCCBP6.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-CSpsf9Z9.js → blade-DFeYrMKk.js} +1 -1
- package/dist/assets/web/dist/assets/{c-DE_b1oWP.js → c-L_QOnFVv.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-CWXd9giL.js → chapel-DLQWUwhu.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-CnMOmWNF.js → cobol-1DlMmrEO.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-B9WvURu8.js → coffee-BTB6os6N.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-BZAJXlWI.js → cpp-BvJI2z3u.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-Bm48wAlb.js → crystal-B11wGW_h.js} +1 -1
- package/dist/assets/web/dist/assets/{css-WtMeC3ab.js → css-BiIWx5Pw.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-B1Mf2JbM.js → dist-CpnyQGOb.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-BmwNa6Ss.js → edge-nSglEXaD.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-BotfP-Mn.js → elixir-x7o8qMl8.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAFc6laN.js → elm-DGrRyqTQ.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-QkOON3bB.js → erb--_dh58Nw.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-BPs-dd73.js → git-rebase-DbpUXwyg.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-D8eooepk.js → glimmer-js-BG0CuZG5.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-B0bRiDVg.js → glimmer-ts-CQIl5qyR.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Bv6bMe1n.js → glsl-9PJNQTcw.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-Bh9y-n9o.js → graphql-CzVTzjEI.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-DSEbn7Ir.js → hack-BhCmAz3Q.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-0NW1-z1f.js → haml-BmI12COR.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-BfgCHFbK.js → handlebars-Cib-ISTS.js} +1 -1
- package/dist/assets/web/dist/assets/{html-CQHc0gQ5.js → html-D3lDapvI.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-B0Tm7wLP.js → html-derivative-DRPY8vOK.js} +1 -1
- package/dist/assets/web/dist/assets/{http-GAuU3aOF.js → http-B9VttK6m.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CrDFbF5B.js → hurl-C0saNJYj.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-BxKswdlK.js → indexRow-DABQtONT.js} +1 -1
- package/dist/assets/web/dist/assets/{java-DYv5KhUe.js → java-BCFbtYAj.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-32SiLGg0.js → javascript-nwBjnrhh.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja--gJDUGYa.js → jinja-CMYO9byN.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-BlKLGzZH.js → jison-Cko8HnE_.js} +1 -1
- package/dist/assets/web/dist/assets/{json-BICanD1N.js → json-Dzv__YsX.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CgW3-uFT.js → jsx-CTFxagih.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-BErRpwwL.js → julia-_Xvten1-.js} +1 -1
- package/dist/assets/web/dist/assets/{just-CordHv0h.js → just-bLps4qOu.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-D7LVPLbi.js → latex-D6UaYsUP.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-3ehz2i4U.js → liquid-CoDIeULa.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-C9ppSVrm.js → lua-D1KFRnVf.js} +1 -1
- package/dist/assets/web/dist/assets/main-C4QfwybO.css +1 -0
- package/dist/assets/web/dist/assets/main-Comxmwi4.js +28 -0
- package/dist/assets/web/dist/assets/{marko-xBZLUBEJ.js → marko-B5N5XcOw.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-BJxGoVgD.js → mdc-uxzjQO3q.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-B-sZmpsb.js → nginx-BMJxs2TK.js} +1 -1
- package/dist/assets/web/dist/assets/{nim--s9wRABK.js → nim-DqQ3-8p1.js} +1 -1
- package/dist/assets/web/dist/assets/{org-DoUvssoI.js → org-DsY6LK59.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-B-vGHKOy.js → perl--pq9iJFp.js} +1 -1
- package/dist/assets/web/dist/assets/{php-CerWx_Xb.js → php-RHCMbsM2.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-CzKrBMb1.js → pug-CFNq_FCp.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-Bs8L8I6k.js → qml-C9Acx9E8.js} +1 -1
- package/dist/assets/web/dist/assets/{r-w6uG6mob.js → r-DxlU8FBd.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-dz2oj2bd.js → razor-CLJ2kKih.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CfQdprnY.js → regexp-CzmdGoEp.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-DAOaX2tb.js → rst-D9yuxBET.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-QcCe4udB.js → ruby-CIRcEvnD.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-BrM4lAAc.js → sas-LOl-uUPh.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-DVbJXoUb.js → scss-BX2_sZHY.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-09fP2hZf.js → shellscript-4d-QvKkY.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-Qgr8xjZF.js → shellsession-DhB16UGy.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-DIYeNTuz.js → soy-Cm1xTUBr.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-CzLR9msV.js → sql-BsfcBJdu.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-BYo2EZKy.js → sseReplay-IzTdD4-3.js} +6 -6
- package/dist/assets/web/dist/assets/{stata-zCY1ZzH0.js → stata-Dwat3BSw.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CJIjTRV4.js → surrealql-P4rtxzAW.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-C5OTPFZg.js → svelte-DTcsi8S8.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DYj-UICF.js → templ-D-kP0Tg5.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-xcie-3Gd.js → tex-D-4vuedK.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-CoN86VTt.js → ts-tags-jEEWYxsu.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-n3-No_Br.js → tsx-CFnlrVzT.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-DPiJ7Vp1.js → twig-D5rGrPjs.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BP6HBaOp.js → typescript-nYIQ35or.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Do9ZjsVP.js → typst-3-UX8dna.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CzAeXNRY.js → vue-CLIPIX03.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-D7iYmYjd.js → vue-html-Ci9CpkHU.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-DtrMRsJx.js → vue-vine-DL5pEYYk.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-DGJHo1c0.js → xml-bO80qikC.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Q00kvNgW.js → xsl-BPze60Ej.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-F-UGKaiO.js → yaml-BQUSon_o.js} +1 -1
- package/dist/cli.js +36177 -34396
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CxTKdpzU.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-C-pUgyyX.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +0 -1
- package/dist/assets/web/dist/assets/main-Cv0q9y56.js +0 -28
- package/dist/assets/web/dist/assets/main-DKFCM-sy.css +0 -1
|
@@ -11,6 +11,13 @@ providers:
|
|
|
11
11
|
anthropic:
|
|
12
12
|
wire: anthropic-messages
|
|
13
13
|
apiKeyEnv: ANTHROPIC_API_KEY
|
|
14
|
+
# The biller's invoice for the daily tie-out on the costs page (costs.md item
|
|
15
|
+
# 4d): each block may name its own invoice API and the env var holding that
|
|
16
|
+
# API's key — an admin or management key, never the inference key. This is
|
|
17
|
+
# where `costs.anthropicAdminKeyEnv` moved; the old key under `costs:` is
|
|
18
|
+
# still accepted for one release.
|
|
19
|
+
# invoiceApi: anthropic-cost-report # GET /v1/organizations/cost_report
|
|
20
|
+
# invoiceKeyEnv: ANTHROPIC_ADMIN_KEY # sk-ant-admin…
|
|
14
21
|
# OpenAI itself runs on its Responses API (record 0052): every first-party
|
|
15
22
|
# OpenAI card in pi's registry is `openai-responses`, OpenAI recommends the
|
|
16
23
|
# wire for new projects, and its chat route loses tool calling with any
|
|
@@ -21,6 +28,8 @@ providers:
|
|
|
21
28
|
wire: openai-responses
|
|
22
29
|
baseUrl: https://api.openai.com/v1
|
|
23
30
|
apiKeyEnv: OPENAI_API_KEY
|
|
31
|
+
# invoiceApi: openai-costs # GET /v1/organization/costs, an admin key
|
|
32
|
+
# invoiceKeyEnv: OPENAI_ADMIN_KEY
|
|
24
33
|
# groq:
|
|
25
34
|
# wire: openai-chat
|
|
26
35
|
# baseUrl: https://api.groq.com/openai/v1
|
|
@@ -42,6 +51,10 @@ providers:
|
|
|
42
51
|
catalog: openrouter
|
|
43
52
|
baseUrl: https://openrouter.ai/api/v1
|
|
44
53
|
apiKeyEnv: OPENROUTER_API_KEY
|
|
54
|
+
# invoiceApi: openrouter-activity # GET /api/v1/activity, a management key;
|
|
55
|
+
# # its `usage` ties against the summed BYOK
|
|
56
|
+
# # fees, `byok_usage_inference` the remainder
|
|
57
|
+
# invoiceKeyEnv: OPENROUTER_MANAGEMENT_KEY
|
|
45
58
|
# The operator's layer over the registry card: what a run needs that no
|
|
46
59
|
# layer names goes out unvouched (and a note says so on the record) until
|
|
47
60
|
# an override like this pins it. `providers check` (a later unit) reads the
|
|
@@ -462,16 +475,17 @@ workspaceDir: ./workspaces
|
|
|
462
475
|
# `tool` (default) forces a call to the router's `route` tool, whose schema is
|
|
463
476
|
# the answer, so prose cannot occur; `text` is the one-JSON-object text contract
|
|
464
477
|
# alone — set it for a provider or model that cannot take a forced tool call.
|
|
465
|
-
# `operator` is the one door's flag (routing-and-config item 29): `
|
|
466
|
-
# (default) — the operator
|
|
467
|
-
# admitted chat event, ahead of stage A, its decision written beside
|
|
468
|
-
# routed request in the run store and nothing run from it; `
|
|
469
|
-
# operator
|
|
478
|
+
# `operator` is the one door's flag (routing-and-config item 29): `on`
|
|
479
|
+
# (default) — the operator's decision is what runs; `shadow` — one operator
|
|
480
|
+
# turn per admitted chat event, ahead of stage A, its decision written beside
|
|
481
|
+
# the routed request in the run store and nothing run from it; `off` — the
|
|
482
|
+
# rollback lever: it turns the operator back off after a bad day, not a choice
|
|
483
|
+
# to make up front. `shadow` and `on` run even under `auto: false`.
|
|
470
484
|
# routing:
|
|
471
485
|
# auto: false
|
|
472
486
|
# model: anthropic/claude-haiku-4-5
|
|
473
487
|
# answer: text
|
|
474
|
-
# operator:
|
|
488
|
+
# operator: off
|
|
475
489
|
|
|
476
490
|
# The thread-reply intake gate (docs/decisions/0058-a-thread-reply-is-read-before-it-is-answered-intake-decides-whether-the-bot-was-addressed.md;
|
|
477
491
|
# docs/reference/specs/routing-and-config.md item 27). How an unmentioned reply
|
|
@@ -493,6 +507,19 @@ workspaceDir: ./workspaces
|
|
|
493
507
|
# threadReplies: classify
|
|
494
508
|
# model: anthropic/claude-haiku-4-5
|
|
495
509
|
|
|
510
|
+
# The orchestration plane (docs/decisions/0064-the-plane-owns-every-runs-state-a-refusal-becomes-a-queue-position-an-ending-is-judged-by-the-ledger-that-saw-it-and-a-release-is-a-quiet-window-a-person-closes.md;
|
|
511
|
+
# docs/reference/specs/routing-and-config.md item 31; orchestration-plane.md).
|
|
512
|
+
# `admission` gates the ledger object's queue decider: `off` (default) — the
|
|
513
|
+
# plane hears of no dispatch and nothing is posted; `shadow` — the bot posts
|
|
514
|
+
# its own outcome per dispatch (`proceeded`, `refused:<code>`) to the ledger
|
|
515
|
+
# object, which logs its decider's decision beside it — nothing runs from the
|
|
516
|
+
# decider; `on` — a later release's flip (the decider's answer is what runs).
|
|
517
|
+
# `reaskMinutes` (default 2) is the plane's one re-ask cadence, used only while
|
|
518
|
+
# something waits on a reporter that fell silent; nothing reads it yet.
|
|
519
|
+
# plane:
|
|
520
|
+
# admission: shadow
|
|
521
|
+
# reaskMinutes: 2
|
|
522
|
+
|
|
496
523
|
# Linked threads (docs/decisions/0037-a-linked-thread-is-quoted-not-joined.md;
|
|
497
524
|
# docs/reference/specs/routing-and-config.md item 22). Off by default. On, a
|
|
498
525
|
# permalink in a request to a thread in another PUBLIC channel the bot is a
|
|
@@ -508,7 +535,8 @@ workspaceDir: ./workspaces
|
|
|
508
535
|
# cloudflareAccountId: <32-hex account id>
|
|
509
536
|
# cloudflareAccountName: acme-infra # optional: how the page names the account beside its share (else the id's first hex)
|
|
510
537
|
# cloudflareTokenEnv: CF_ANALYTICS_TOKEN # token with Account Analytics:Read
|
|
511
|
-
# anthropicAdminKeyEnv: ANTHROPIC_ADMIN_KEY #
|
|
538
|
+
# anthropicAdminKeyEnv: ANTHROPIC_ADMIN_KEY # legacy (one release): moved to providers.anthropic.invoiceKeyEnv
|
|
539
|
+
# # (invoiceApi: anthropic-cost-report); still feeds the LLM line here
|
|
512
540
|
# snapshot: # the page serves a snapshot of both billing sources, never a live read;
|
|
513
541
|
# everyHours: 24 # how often the bot reads them (1–168, default 24). `costs snapshot`
|
|
514
542
|
# # takes one now. Kept on the state Worker any `*.worker` block names
|
|
@@ -536,12 +564,13 @@ workspaceDir: ./workspaces
|
|
|
536
564
|
# Run metrics (docs/reference/specs/run-metrics.md). Optional. The Analytics Engine
|
|
537
565
|
# dataset every finished run's point lands in — the SAME name the deployment
|
|
538
566
|
# profile's `metrics.dataset` binds on the state Worker as RUN_METRICS (the bot
|
|
539
|
-
# warns at boot when the two differ; neither side refuses to start).
|
|
540
|
-
#
|
|
541
|
-
# capability
|
|
542
|
-
#
|
|
567
|
+
# warns at boot when the two differ; neither side refuses to start). With the
|
|
568
|
+
# `costs` block's Cloudflare account and analytics token this turns the `metrics`
|
|
569
|
+
# capability on: `metrics trend` reads the trend report from the dataset over the
|
|
570
|
+
# Analytics Engine SQL API, weighted for sampling.
|
|
543
571
|
# metrics:
|
|
544
572
|
# dataset: switchboard_runs
|
|
573
|
+
# days: 30 # optional: the default report range in whole UTC days, 1..90
|
|
545
574
|
|
|
546
575
|
# Delivery indicators: GET /delivery and `delivery report` (docs/reference/specs/delivery.md).
|
|
547
576
|
# Optional. Needs a GitHub credential (the App triple or GH_TOKEN); read-only on GitHub.
|
|
@@ -78,6 +78,18 @@ import {
|
|
|
78
78
|
selectReclaim,
|
|
79
79
|
} from "../../src/core/runLedger/decisions.ts";
|
|
80
80
|
import { intakeReceiptRetentionMs } from "../../src/core/budgets.ts";
|
|
81
|
+
import {
|
|
82
|
+
decide,
|
|
83
|
+
planeAskWordOf,
|
|
84
|
+
type PlaneAckOutcome,
|
|
85
|
+
type PlaneEffect,
|
|
86
|
+
type PlaneEvent,
|
|
87
|
+
type PlaneOutcomePost,
|
|
88
|
+
type PlaneQueueRow,
|
|
89
|
+
type PlaneStage,
|
|
90
|
+
type PlaneState,
|
|
91
|
+
type PlaneWrite,
|
|
92
|
+
} from "../../src/core/plane/decide.ts";
|
|
81
93
|
import {
|
|
82
94
|
IDEMPOTENCY_KEY_PATTERN,
|
|
83
95
|
capThreadEvent,
|
|
@@ -1206,13 +1218,16 @@ export class CostsSnapshotDO extends DurableObject<Env> {
|
|
|
1206
1218
|
`);
|
|
1207
1219
|
}
|
|
1208
1220
|
|
|
1209
|
-
/** Replace the snapshot whole: every part rewritten in one transaction.
|
|
1221
|
+
/** Replace the snapshot whole: every part rewritten in one transaction. Each
|
|
1222
|
+
* logical part is its own named row — `invoices` included, never a rest-spread
|
|
1223
|
+
* into the meta row that would silently absorb future fields. */
|
|
1210
1224
|
async put(snapshot: CostsSnapshot): Promise<void> {
|
|
1211
|
-
const { usage, llm, runUsage, ...meta } = snapshot;
|
|
1225
|
+
const { usage, llm, invoices, runUsage, ...meta } = snapshot;
|
|
1212
1226
|
const rows: Array<[string, unknown]> = [
|
|
1213
1227
|
["meta", meta],
|
|
1214
1228
|
...USAGE_PARTS.map((name): [string, unknown] => [`usage.${name}`, usage[name]]),
|
|
1215
1229
|
["llm", llm],
|
|
1230
|
+
...(invoices !== undefined ? [["invoices", invoices] as [string, unknown]] : []),
|
|
1216
1231
|
["runUsage", runUsage],
|
|
1217
1232
|
];
|
|
1218
1233
|
this.ctx.storage.transactionSync(() => {
|
|
@@ -1237,10 +1252,12 @@ export class CostsSnapshotDO extends DurableObject<Env> {
|
|
|
1237
1252
|
return body === undefined ? undefined : (JSON.parse(body) as unknown);
|
|
1238
1253
|
};
|
|
1239
1254
|
const usage = Object.fromEntries(USAGE_PARTS.map((name) => [name, read(`usage.${name}`)]));
|
|
1255
|
+
const invoices = read("invoices");
|
|
1240
1256
|
const snapshot = {
|
|
1241
1257
|
...(JSON.parse(meta) as Record<string, unknown>),
|
|
1242
1258
|
usage,
|
|
1243
1259
|
llm: read("llm"),
|
|
1260
|
+
...(invoices !== undefined ? { invoices } : {}),
|
|
1244
1261
|
runUsage: read("runUsage"),
|
|
1245
1262
|
};
|
|
1246
1263
|
return isCostsSnapshot(snapshot) ? snapshot : null;
|
|
@@ -1514,7 +1531,20 @@ function rowToLive(r: LiveRow): LiveRunRow {
|
|
|
1514
1531
|
};
|
|
1515
1532
|
}
|
|
1516
1533
|
|
|
1517
|
-
type HeartbeatAnswer = FenceResult & { stop?: StopMode | null; phase?: LivePhase };
|
|
1534
|
+
type HeartbeatAnswer = FenceResult & { stop?: StopMode | null; phase?: LivePhase; effects?: PlaneEffect[] };
|
|
1535
|
+
|
|
1536
|
+
/** Whether the bot's outcome and the decider's word agree (orchestration-plane item 8): `proceeded`
|
|
1537
|
+
* beside `proceed`, and a thread-live refusal beside `queued` — the refusal
|
|
1538
|
+
* IS the queue position the plane would hold. `null` for a pair the decider
|
|
1539
|
+
* does not model yet: logged, never counted. */
|
|
1540
|
+
function planeAgreementOf(outcome: string, decider: "proceed" | "queued"): boolean | null {
|
|
1541
|
+
if (outcome === "proceeded") return decider === "proceed";
|
|
1542
|
+
if (outcome === "refused:thread-live") return decider === "queued";
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/** The most effects one answer carries (record 0064; orchestration-plane item 7): the rest ride the next heartbeat. */
|
|
1547
|
+
const PLANE_EFFECTS_PER_ANSWER = 32;
|
|
1518
1548
|
|
|
1519
1549
|
export class RunHistoryDO extends DurableObject<Env> {
|
|
1520
1550
|
private readonly sql: SqlStorage;
|
|
@@ -1681,6 +1711,215 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1681
1711
|
PRIMARY KEY (instance_id, unit, seq)
|
|
1682
1712
|
);
|
|
1683
1713
|
`);
|
|
1714
|
+
// The orchestration plane's tables (record 0064; orchestration-plane.md item 7):
|
|
1715
|
+
// the queue with its conditions, the reservations, the quiet and pressure
|
|
1716
|
+
// windows, the watches' findings, the offered effects and the residents'
|
|
1717
|
+
// levels. This unit opens them all so a later Worker and an earlier one agree on
|
|
1718
|
+
// the schema; only the queue and the effects are written yet.
|
|
1719
|
+
this.sql.exec(`
|
|
1720
|
+
CREATE TABLE IF NOT EXISTS plane_queue (
|
|
1721
|
+
run_id TEXT PRIMARY KEY,
|
|
1722
|
+
requester TEXT NOT NULL,
|
|
1723
|
+
thread_key TEXT NOT NULL,
|
|
1724
|
+
stage TEXT NOT NULL,
|
|
1725
|
+
request_json TEXT NOT NULL,
|
|
1726
|
+
conditions_json TEXT NOT NULL,
|
|
1727
|
+
position_at INTEGER NOT NULL,
|
|
1728
|
+
queued_at INTEGER NOT NULL,
|
|
1729
|
+
state TEXT NOT NULL
|
|
1730
|
+
);
|
|
1731
|
+
CREATE INDEX IF NOT EXISTS plane_queue_waiting ON plane_queue(state, queued_at);
|
|
1732
|
+
CREATE TABLE IF NOT EXISTS plane_reservations (
|
|
1733
|
+
kind TEXT NOT NULL,
|
|
1734
|
+
key TEXT NOT NULL,
|
|
1735
|
+
run_id TEXT NOT NULL,
|
|
1736
|
+
at INTEGER NOT NULL,
|
|
1737
|
+
PRIMARY KEY (kind, key)
|
|
1738
|
+
);
|
|
1739
|
+
CREATE TABLE IF NOT EXISTS plane_windows (
|
|
1740
|
+
kind TEXT NOT NULL,
|
|
1741
|
+
key TEXT NOT NULL,
|
|
1742
|
+
phase TEXT NOT NULL,
|
|
1743
|
+
opened_at INTEGER NOT NULL,
|
|
1744
|
+
reason_json TEXT NOT NULL,
|
|
1745
|
+
PRIMARY KEY (kind, key)
|
|
1746
|
+
);
|
|
1747
|
+
CREATE TABLE IF NOT EXISTS plane_findings (
|
|
1748
|
+
id TEXT PRIMARY KEY,
|
|
1749
|
+
watch TEXT NOT NULL,
|
|
1750
|
+
subject TEXT NOT NULL,
|
|
1751
|
+
timeline_json TEXT NOT NULL,
|
|
1752
|
+
filed_at INTEGER NOT NULL
|
|
1753
|
+
);
|
|
1754
|
+
CREATE TABLE IF NOT EXISTS plane_effects (
|
|
1755
|
+
id TEXT PRIMARY KEY,
|
|
1756
|
+
body_json TEXT NOT NULL,
|
|
1757
|
+
offered_at INTEGER NOT NULL,
|
|
1758
|
+
acked_at INTEGER
|
|
1759
|
+
);
|
|
1760
|
+
CREATE INDEX IF NOT EXISTS plane_effects_open ON plane_effects(acked_at, offered_at);
|
|
1761
|
+
CREATE TABLE IF NOT EXISTS plane_levels (
|
|
1762
|
+
resident TEXT NOT NULL,
|
|
1763
|
+
name TEXT NOT NULL,
|
|
1764
|
+
side TEXT NOT NULL,
|
|
1765
|
+
reported_at INTEGER NOT NULL,
|
|
1766
|
+
generation TEXT NOT NULL,
|
|
1767
|
+
PRIMARY KEY (resident, name)
|
|
1768
|
+
);
|
|
1769
|
+
`);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// ---- the orchestration plane (record 0064; orchestration-plane.md) ----------
|
|
1773
|
+
|
|
1774
|
+
/** The decider's state, read inside the caller's `transactionSync`: the
|
|
1775
|
+
* queue oldest first and the threads a live row holds. `excludeRunId` drops
|
|
1776
|
+
* that run's own live row from the view — a shadow post judged after the
|
|
1777
|
+
* dispatch it describes claimed the thread must not read its own claim as
|
|
1778
|
+
* "thread live" (orchestration-plane item 8). */
|
|
1779
|
+
private planeState(excludeRunId?: string): PlaneState {
|
|
1780
|
+
const queue = this.sql
|
|
1781
|
+
.exec<{
|
|
1782
|
+
run_id: string;
|
|
1783
|
+
requester: string;
|
|
1784
|
+
thread_key: string;
|
|
1785
|
+
stage: string;
|
|
1786
|
+
request_json: string;
|
|
1787
|
+
conditions_json: string;
|
|
1788
|
+
position_at: number;
|
|
1789
|
+
queued_at: number;
|
|
1790
|
+
state: string;
|
|
1791
|
+
}>(`SELECT * FROM plane_queue ORDER BY queued_at ASC, run_id ASC`)
|
|
1792
|
+
.toArray()
|
|
1793
|
+
.map((r): PlaneQueueRow => ({
|
|
1794
|
+
runId: r.run_id,
|
|
1795
|
+
requester: r.requester,
|
|
1796
|
+
threadKey: r.thread_key,
|
|
1797
|
+
stage: r.stage as PlaneStage,
|
|
1798
|
+
request: JSON.parse(r.request_json) as Record<string, unknown>,
|
|
1799
|
+
conditions: JSON.parse(r.conditions_json) as PlaneQueueRow["conditions"],
|
|
1800
|
+
position: r.position_at,
|
|
1801
|
+
queuedAt: r.queued_at,
|
|
1802
|
+
state: r.state as PlaneQueueRow["state"],
|
|
1803
|
+
}));
|
|
1804
|
+
const liveThreads = this.sql
|
|
1805
|
+
.exec<{ thread_key: string }>(`SELECT thread_key FROM live_runs WHERE run_id IS NOT ?`, excludeRunId ?? null)
|
|
1806
|
+
.toArray()
|
|
1807
|
+
.map((r) => r.thread_key);
|
|
1808
|
+
return { queue, liveThreads };
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/** The decider's writes, applied inside the same `transactionSync` that read
|
|
1812
|
+
* the state — the decision and its consequences land together (orchestration-plane item 6). */
|
|
1813
|
+
private applyPlaneWrites(writes: PlaneWrite[]): void {
|
|
1814
|
+
for (const w of writes) {
|
|
1815
|
+
if (w.table === "plane_queue" && w.op === "put") {
|
|
1816
|
+
this.sql.exec(
|
|
1817
|
+
`INSERT OR REPLACE INTO plane_queue
|
|
1818
|
+
(run_id, requester, thread_key, stage, request_json, conditions_json, position_at, queued_at, state)
|
|
1819
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1820
|
+
w.row.runId,
|
|
1821
|
+
w.row.requester,
|
|
1822
|
+
w.row.threadKey,
|
|
1823
|
+
w.row.stage,
|
|
1824
|
+
JSON.stringify(w.row.request),
|
|
1825
|
+
JSON.stringify(w.row.conditions),
|
|
1826
|
+
w.row.position,
|
|
1827
|
+
w.row.queuedAt,
|
|
1828
|
+
w.row.state,
|
|
1829
|
+
);
|
|
1830
|
+
} else if (w.table === "plane_queue" && w.op === "state") {
|
|
1831
|
+
this.sql.exec(`UPDATE plane_queue SET state = ? WHERE run_id = ?`, w.state, w.runId);
|
|
1832
|
+
} else {
|
|
1833
|
+
// An offer keeps its first `offered_at`: a re-decided admit after a
|
|
1834
|
+
// roll is the same effect, not a younger one.
|
|
1835
|
+
this.sql.exec(
|
|
1836
|
+
`INSERT OR IGNORE INTO plane_effects (id, body_json, offered_at, acked_at) VALUES (?, ?, ?, NULL)`,
|
|
1837
|
+
w.effect.id,
|
|
1838
|
+
JSON.stringify(w.effect),
|
|
1839
|
+
w.at,
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/** Apply one plane event: state read, decider, writes and effects in ONE
|
|
1846
|
+
* `transactionSync` (orchestration-plane item 6). The transport unit's routes feed it; the tests pin the atomicity. */
|
|
1847
|
+
planeApply(event: PlaneEvent): { effects: PlaneEffect[] } {
|
|
1848
|
+
let effects: PlaneEffect[] = [];
|
|
1849
|
+
this.ctx.storage.transactionSync(() => {
|
|
1850
|
+
const decision = decide(this.planeState(), event);
|
|
1851
|
+
this.applyPlaneWrites(decision.writes);
|
|
1852
|
+
effects = decision.effects;
|
|
1853
|
+
});
|
|
1854
|
+
return { effects };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/** The unacknowledged effects, oldest first, at most `PLANE_EFFECTS_PER_ANSWER`
|
|
1858
|
+
* (orchestration-plane item 7) — what every heartbeat and reclaim answer carries. Public: the
|
|
1859
|
+
* reclaim route composes it beside the runs it took. */
|
|
1860
|
+
openPlaneEffects(): PlaneEffect[] {
|
|
1861
|
+
return this.sql
|
|
1862
|
+
.exec<{ body_json: string }>(
|
|
1863
|
+
`SELECT body_json FROM plane_effects WHERE acked_at IS NULL ORDER BY offered_at ASC, id ASC LIMIT ?`,
|
|
1864
|
+
PLANE_EFFECTS_PER_ANSWER,
|
|
1865
|
+
)
|
|
1866
|
+
.toArray()
|
|
1867
|
+
.map((r) => JSON.parse(r.body_json) as PlaneEffect);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
/** Shadow (orchestration-plane item 8): the bot's own outcome for one dispatch, judged beside the
|
|
1871
|
+
* decider's word for the same ask. Nothing runs and nothing queues from
|
|
1872
|
+
* the decider here — the comparison is logged, and a disagreement bumps a
|
|
1873
|
+
* per-condition counter in `meta` for the `plane disagreements` line. An
|
|
1874
|
+
* outcome the decider does not model yet (an allowlist refusal, a cold
|
|
1875
|
+
* fall) is logged uncounted. The post is fired without an await, so it can
|
|
1876
|
+
* arrive AFTER the dispatch it describes claimed this thread's `live_runs`
|
|
1877
|
+
* row; a post carrying the run's own id excludes that row from the live
|
|
1878
|
+
* view so the run's own claim never reads as a false disagreement. */
|
|
1879
|
+
planeOutcome(
|
|
1880
|
+
post: PlaneOutcomePost,
|
|
1881
|
+
now: number,
|
|
1882
|
+
): { ok: true; decider: "proceed" | "queued"; agreed: boolean | null } {
|
|
1883
|
+
let decider: "proceed" | "queued" = "proceed";
|
|
1884
|
+
let agreed: boolean | null = null;
|
|
1885
|
+
this.ctx.storage.transactionSync(() => {
|
|
1886
|
+
const ask: PlaneEvent = {
|
|
1887
|
+
kind: "ask",
|
|
1888
|
+
at: now,
|
|
1889
|
+
runId: post.runId ?? `ask:${post.threadKey}:${now}`,
|
|
1890
|
+
requester: post.requester,
|
|
1891
|
+
threadKey: post.threadKey,
|
|
1892
|
+
stage: post.stage,
|
|
1893
|
+
request: {},
|
|
1894
|
+
};
|
|
1895
|
+
decider = planeAskWordOf(decide(this.planeState(post.runId), ask), ask.runId);
|
|
1896
|
+
agreed = planeAgreementOf(post.outcome, decider);
|
|
1897
|
+
if (agreed === false) this.bumpPlaneDisagreement("thread_free");
|
|
1898
|
+
});
|
|
1899
|
+
console.log(
|
|
1900
|
+
`[plane/outcome] ${post.threadKey} ${post.stage} bot=${post.outcome} decider=${decider} agreed=${agreed ?? "uncompared"}`,
|
|
1901
|
+
);
|
|
1902
|
+
return { ok: true, decider, agreed };
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/** The per-condition disagreement counts (orchestration-plane item 8), kept in `meta` so the table's
|
|
1906
|
+
* later `plane disagreements` line can read them. */
|
|
1907
|
+
private bumpPlaneDisagreement(condition: string): void {
|
|
1908
|
+
const row = this.sql
|
|
1909
|
+
.exec<{ value: string }>(`SELECT value FROM meta WHERE key = 'plane_disagreements'`)
|
|
1910
|
+
.toArray()[0];
|
|
1911
|
+
const counts = row ? (JSON.parse(row.value) as Record<string, number>) : {};
|
|
1912
|
+
counts[condition] = (counts[condition] ?? 0) + 1;
|
|
1913
|
+
this.sql.exec(`INSERT OR REPLACE INTO meta (key, value) VALUES ('plane_disagreements', ?)`, JSON.stringify(counts));
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
/** An effect's acknowledgement by id (orchestration-plane item 7): `done` and `skipped` close it,
|
|
1917
|
+
* `deferred` leaves it offered for the next answer. An unknown id is a
|
|
1918
|
+
* no-op — the bot may ack an effect an older table never held. */
|
|
1919
|
+
planeAck(id: string, outcome: PlaneAckOutcome, now: number): { ok: true } {
|
|
1920
|
+
if (outcome !== "deferred")
|
|
1921
|
+
this.sql.exec(`UPDATE plane_effects SET acked_at = ? WHERE id = ? AND acked_at IS NULL`, now, id);
|
|
1922
|
+
return { ok: true };
|
|
1684
1923
|
}
|
|
1685
1924
|
|
|
1686
1925
|
// ---- the coordinator's parent records (run-history item 49) -----------------
|
|
@@ -1969,7 +2208,10 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1969
2208
|
return;
|
|
1970
2209
|
}
|
|
1971
2210
|
this.sql.exec(`UPDATE live_runs SET lease_until = ? WHERE run_id = ?`, now + leaseMs, runId);
|
|
1972
|
-
|
|
2211
|
+
// The plane's open effects ride every owner's heartbeat answer (record
|
|
2212
|
+
// 0064; orchestration-plane item 7) — empty until a unit writes them, but always present, so the
|
|
2213
|
+
// client's ack loop needs no version probe.
|
|
2214
|
+
out = { ok: true, stop: row.stop, phase: row.phase, effects: this.openPlaneEffects() };
|
|
1973
2215
|
});
|
|
1974
2216
|
return out;
|
|
1975
2217
|
}
|
|
@@ -3931,6 +4173,60 @@ const LEDGER_ROUTES = new Set([
|
|
|
3931
4173
|
"/runs/session/notepad/write",
|
|
3932
4174
|
]);
|
|
3933
4175
|
|
|
4176
|
+
/** The plane's routes (record 0064; orchestration-plane items 7 and 8): the shadow outcome post and the
|
|
4177
|
+
* effect acknowledgement. Both land on the ledger object of the given store
|
|
4178
|
+
* key, like every `/runs/*` route. */
|
|
4179
|
+
const PLANE_ROUTES = new Set(["/plane/outcome", "/plane/ack"]);
|
|
4180
|
+
|
|
4181
|
+
const PLANE_STAGES = new Set(["admission", "runner", "resident"]);
|
|
4182
|
+
const PLANE_OUTCOME = /^(proceeded|refused:[a-z0-9-]+|fell_cold:[a-z0-9_-]+)$/;
|
|
4183
|
+
const PLANE_ACK_OUTCOMES = new Set(["done", "skipped", "deferred"]);
|
|
4184
|
+
|
|
4185
|
+
/** The `/plane/*` routes: validated before any object call, ids and words
|
|
4186
|
+
* only on the log lines. */
|
|
4187
|
+
async function handlePlane(pathname: string, body: unknown, env: Env): Promise<Response> {
|
|
4188
|
+
if (typeof body !== "object" || body === null) return json({ error: "body must be a JSON object" }, 400);
|
|
4189
|
+
const b = body as Record<string, unknown>;
|
|
4190
|
+
const key = parseStoreKey(b);
|
|
4191
|
+
if (!key.ok) return json({ error: key.error }, 400);
|
|
4192
|
+
const stub = env.RUNS.get(env.RUNS.idFromName(key.value));
|
|
4193
|
+
const now = systemClock();
|
|
4194
|
+
if (pathname === "/plane/outcome") {
|
|
4195
|
+
if (typeof b.threadKey !== "string" || b.threadKey.length === 0)
|
|
4196
|
+
return json({ error: "threadKey must be a non-empty string" }, 400);
|
|
4197
|
+
if (typeof b.requester !== "string" || b.requester.length === 0)
|
|
4198
|
+
return json({ error: "requester must be a non-empty string" }, 400);
|
|
4199
|
+
if (typeof b.stage !== "string" || !PLANE_STAGES.has(b.stage))
|
|
4200
|
+
return json({ error: "stage must be admission, runner or resident" }, 400);
|
|
4201
|
+
if (typeof b.outcome !== "string" || !PLANE_OUTCOME.test(b.outcome))
|
|
4202
|
+
return json({ error: "outcome must be proceeded, refused:<code> or fell_cold:<token>" }, 400);
|
|
4203
|
+
let runId: string | undefined;
|
|
4204
|
+
if (b.runId !== undefined) {
|
|
4205
|
+
const parsed = parseRunId(b.runId);
|
|
4206
|
+
if (!parsed.ok) return json({ error: parsed.error }, 400);
|
|
4207
|
+
runId = parsed.value;
|
|
4208
|
+
}
|
|
4209
|
+
const r = await stub.planeOutcome(
|
|
4210
|
+
{
|
|
4211
|
+
...(runId !== undefined ? { runId } : {}),
|
|
4212
|
+
requester: b.requester,
|
|
4213
|
+
threadKey: b.threadKey,
|
|
4214
|
+
stage: b.stage as PlaneStage,
|
|
4215
|
+
outcome: b.outcome,
|
|
4216
|
+
},
|
|
4217
|
+
now,
|
|
4218
|
+
);
|
|
4219
|
+
return json(r);
|
|
4220
|
+
}
|
|
4221
|
+
if (pathname === "/plane/ack") {
|
|
4222
|
+
if (typeof b.id !== "string" || b.id.length === 0) return json({ error: "id must be a non-empty string" }, 400);
|
|
4223
|
+
if (typeof b.outcome !== "string" || !PLANE_ACK_OUTCOMES.has(b.outcome))
|
|
4224
|
+
return json({ error: "outcome must be done, skipped or deferred" }, 400);
|
|
4225
|
+
return json(await stub.planeAck(b.id, b.outcome as PlaneAckOutcome, now));
|
|
4226
|
+
}
|
|
4227
|
+
return json({ error: "not found" }, 404);
|
|
4228
|
+
}
|
|
4229
|
+
|
|
3934
4230
|
/** Routes whose bodies may carry a record, a transcript chunk, or an event batch. */
|
|
3935
4231
|
const WIDE_BODY_ROUTES = new Set([
|
|
3936
4232
|
"/runs/put",
|
|
@@ -4237,7 +4533,9 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
|
|
|
4237
4533
|
// declared shape.
|
|
4238
4534
|
const runs = (await stub.reclaim(g.value, at, lease.value)) as unknown as ReclaimedRun[];
|
|
4239
4535
|
console.log(`[runs/reclaim] ${key.value} ${g.value} took ${runs.length} run(s)`);
|
|
4240
|
-
|
|
4536
|
+
// The reclaim sweep's answer carries the plane's open effects like every
|
|
4537
|
+
// heartbeat answer does (record 0064; orchestration-plane item 7) — empty until a unit writes them.
|
|
4538
|
+
return json({ runs, effects: await stub.openPlaneEffects() });
|
|
4241
4539
|
}
|
|
4242
4540
|
if (pathname === "/runs/handoff") {
|
|
4243
4541
|
const g = gen(b.gen);
|
|
@@ -4372,7 +4670,10 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
|
|
|
4372
4670
|
if (pathname === "/runs/heartbeat") {
|
|
4373
4671
|
const lease = parseLeaseMs(b.leaseMs);
|
|
4374
4672
|
if (!lease.ok) return json({ error: lease.error }, 400);
|
|
4375
|
-
|
|
4673
|
+
// The RPC type mapping reads the effects' open-ended `request` JSON as
|
|
4674
|
+
// unserializable; the values are plain JSON, so the cast only restores the
|
|
4675
|
+
// declared shape (as the reclaim route's does).
|
|
4676
|
+
const r = (await stub.heartbeat(runId.value, g.value, lease.value, now)) as unknown as HeartbeatAnswer;
|
|
4376
4677
|
return r.ok ? json(r) : json(r, 409);
|
|
4377
4678
|
}
|
|
4378
4679
|
if (pathname === "/runs/append") {
|
|
@@ -4504,6 +4805,7 @@ const ROUTES = new Set([
|
|
|
4504
4805
|
"/runs/usage",
|
|
4505
4806
|
"/runs/delete",
|
|
4506
4807
|
...LEDGER_ROUTES,
|
|
4808
|
+
...PLANE_ROUTES,
|
|
4507
4809
|
]);
|
|
4508
4810
|
|
|
4509
4811
|
/** The two decisions `fetch` makes once and hands down: is the path one of
|
|
@@ -4519,7 +4821,7 @@ interface Admission {
|
|
|
4519
4821
|
* dataset (run-metrics.md item 5) — the name from the `RUN_METRICS_DATASET`
|
|
4520
4822
|
* var rendered beside the binding, so the probe can compare it to the bot's. */
|
|
4521
4823
|
export function featuresOf(env: Pick<Env, "RUN_METRICS" | "RUN_METRICS_DATASET">): string[] {
|
|
4522
|
-
const features = ["memory", "schedules", "runs", "config", "delivery", "costs"];
|
|
4824
|
+
const features = ["memory", "schedules", "runs", "config", "delivery", "costs", "plane"];
|
|
4523
4825
|
if (env.RUN_METRICS !== undefined) features.push(`runMetrics:${env.RUN_METRICS_DATASET ?? "unknown"}`);
|
|
4524
4826
|
return features;
|
|
4525
4827
|
}
|
|
@@ -4578,6 +4880,7 @@ async function handleRequest(request: Request, env: Env, admission: Admission):
|
|
|
4578
4880
|
return json({ firings });
|
|
4579
4881
|
}
|
|
4580
4882
|
if (url.pathname.startsWith("/runs/")) return handleRuns(url.pathname, body, env);
|
|
4883
|
+
if (url.pathname.startsWith("/plane/")) return handlePlane(url.pathname, body, env);
|
|
4581
4884
|
if (url.pathname.startsWith("/config/")) return handleConfig(url.pathname, body, env);
|
|
4582
4885
|
if (url.pathname.startsWith("/delivery/")) return handleDelivery(url.pathname, body, env);
|
|
4583
4886
|
if (url.pathname.startsWith("/costs/")) return handleCosts(url.pathname, body, env);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.254.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.254.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -20445,7 +20445,7 @@
|
|
|
20445
20445
|
},
|
|
20446
20446
|
"packages/switchboard": {
|
|
20447
20447
|
"name": "@coreplane/switchboard",
|
|
20448
|
-
"version": "1.
|
|
20448
|
+
"version": "1.254.0",
|
|
20449
20449
|
"license": "Apache-2.0",
|
|
20450
20450
|
"dependencies": {
|
|
20451
20451
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.254.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/source.json
CHANGED
|
@@ -60,6 +60,12 @@ export const CHAT_OPEN_ACTIONS: readonly string[] = [
|
|
|
60
60
|
"schedule:read",
|
|
61
61
|
"memory:write",
|
|
62
62
|
"mcp:write",
|
|
63
|
+
// `steer.run` (chat-only): the door admits every chat user because the real
|
|
64
|
+
// fence is the steer OWNER rule at the wired sender — the named run's
|
|
65
|
+
// requester, an id their identity record links, or a `runs:write` grant
|
|
66
|
+
// (authorization item 16a) — which the command door cannot see. Never held
|
|
67
|
+
// by a token: the machine surfaces have `send_to_run` and the ingress.
|
|
68
|
+
"steer:write",
|
|
63
69
|
];
|
|
64
70
|
|
|
65
71
|
/** The two writes a browser session holds beside its reads (record 0043): the
|
|
@@ -58,6 +58,11 @@ export const POLICY: readonly Rule[] = [
|
|
|
58
58
|
// List-shaped `runs.*`: the grant admits the command; the store predicate narrows the rows.
|
|
59
59
|
{ action: "runs:read", resource: "command", when: [grant("runs:read")] },
|
|
60
60
|
{ action: "runs:write", resource: "command", when: [grant("runs:write")] },
|
|
61
|
+
// `steer.run`: every chat user holds the grant (CHAT_OPEN_ACTIONS) because
|
|
62
|
+
// the fence is the steer owner rule at the wired sender, decided against the
|
|
63
|
+
// NAMED run (`authorizeSteerOwner`, authorization item 16a) — the requester,
|
|
64
|
+
// a linked id, or `runs:write` — which no command-door row can see.
|
|
65
|
+
{ action: "steer:write", resource: "command", when: [grant("steer:write")] },
|
|
61
66
|
|
|
62
67
|
// ── conversations ────────────────────────────────────────────────────────
|
|
63
68
|
// Who may point the bot at another thread (record 0037). Asked for a
|
|
@@ -91,6 +96,12 @@ export const POLICY: readonly Rule[] = [
|
|
|
91
96
|
// `all` and a named `grants` entry hold it).
|
|
92
97
|
{ action: "costs:write", resource: "command", when: [grant("costs:write")] },
|
|
93
98
|
|
|
99
|
+
// ── metrics ──────────────────────────────────────────────────────────────
|
|
100
|
+
// `metrics trend` reads the run-metrics dataset's report: the `costs:read`
|
|
101
|
+
// shape — a browser session's baseline, a grant for a Slack user or a token,
|
|
102
|
+
// never a chat baseline.
|
|
103
|
+
{ action: "metrics:read", resource: "command", when: [grant("metrics:read")] },
|
|
104
|
+
|
|
94
105
|
// ── providers ────────────────────────────────────────────────────────────
|
|
95
106
|
// `providers check` reads the provider's own endpoints on request: a browser
|
|
96
107
|
// session's read baseline, a grant everywhere else — an on-request provider
|
|
@@ -38,6 +38,14 @@ export const WEEK_MS = 7 * DAY_MS;
|
|
|
38
38
|
* which stamps the expiry on its own clock. */
|
|
39
39
|
export const CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
|
|
40
40
|
|
|
41
|
+
/** How long a question's Yes stays pending (docs/decisions/0054, as amended;
|
|
42
|
+
* docs/reference/specs/routing-and-config.md item 25): one day. A Yes only
|
|
43
|
+
* re-dispatches the corrected message, which is then routed, authorized and,
|
|
44
|
+
* for a write, offered its own graded button — the write's ten-minute
|
|
45
|
+
* freshness is enforced downstream, so a short window here protects nothing
|
|
46
|
+
* and only loses answers. People answer a question in minutes to hours. */
|
|
47
|
+
export const QUESTION_TTL_MS = DAY_MS;
|
|
48
|
+
|
|
41
49
|
/** How long a dispatch's FIRST attach to a resident — a fresh run's, or a
|
|
42
50
|
* resumed run's re-attach to its recorded worktree — waits for the resident
|
|
43
51
|
* to wake when the Worker typed its refusal as the platform's transient
|
|
@@ -390,6 +390,11 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
390
390
|
prNumber,
|
|
391
391
|
url,
|
|
392
392
|
...(typeof headSha === "string" ? { headSha } : {}),
|
|
393
|
+
// The entry facts (issue 1689): the branch's own tip and whether the
|
|
394
|
+
// bot's approval stands at the head, read at the unit-start's pre-check
|
|
395
|
+
// so a re-issued plan resumes at review or at the merge decision.
|
|
396
|
+
...(typeof a.body.branchHead === "string" ? { branchHead: a.body.branchHead } : {}),
|
|
397
|
+
...(typeof a.body.approved === "boolean" ? { approved: a.body.approved } : {}),
|
|
393
398
|
...(typeof a.body.autoMergeEnabled === "boolean" ? { autoMergeEnabled: a.body.autoMergeEnabled } : {}),
|
|
394
399
|
...(isCommitChecks(a.body.checks) ? { checks: a.body.checks } : {}),
|
|
395
400
|
// The ready-state facts beside the checks (agent-ship item 9): the
|
|
@@ -584,6 +589,10 @@ async function perform(
|
|
|
584
589
|
await step.do(action.step, STEP_CONFIG, () =>
|
|
585
590
|
call(bot, "pr-check", {
|
|
586
591
|
...tag,
|
|
592
|
+
// `entry` is the unit-start's pre-check (issue 1689): the bot
|
|
593
|
+
// reads the branch's tip, the approval and the checks beside the
|
|
594
|
+
// listing, so a re-issued plan's unit resumes instead of recoding.
|
|
595
|
+
...(action.entry === true ? { entry: true } : {}),
|
|
587
596
|
...(action.recover !== undefined ? { recover: action.recover } : {}),
|
|
588
597
|
...(action.pr !== undefined ? { pr: action.pr } : {}),
|
|
589
598
|
}),
|