@coreplane/switchboard 1.252.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 +54 -1
- package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +455 -32
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
- package/dist/assets/deploy/profile.example.json +1 -1
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/authz/grants.ts +6 -0
- package/dist/assets/src/core/authz/policy.ts +19 -0
- package/dist/assets/src/core/budgets.ts +25 -0
- package/dist/assets/src/core/coordinator/contract.ts +51 -1
- package/dist/assets/src/core/coordinator/driver.ts +111 -11
- package/dist/assets/src/core/costs.ts +221 -2
- package/dist/assets/src/core/costsSnapshotStore.ts +22 -1
- package/dist/assets/src/core/memory/engine.ts +25 -4
- package/dist/assets/src/core/memory/types.ts +6 -2
- package/dist/assets/src/core/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 +57 -1
- package/dist/assets/src/core/runFriction.ts +1 -0
- package/dist/assets/src/core/runLedger/transcript.ts +23 -3
- package/dist/assets/src/core/runMetrics.ts +237 -0
- package/dist/assets/src/core/runRecord.ts +61 -0
- package/dist/assets/src/core/runUsage.ts +41 -9
- package/dist/assets/src/core/ship/contract.ts +8 -15
- package/dist/assets/src/core/ship/coordinator.ts +150 -11
- package/dist/assets/src/deploy/profile.ts +16 -0
- package/dist/assets/web/dist/.vite/manifest.json +461 -427
- package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.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-3ELQWM0r.js → DeliveryPage-ngPsO2to.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-DvxTHzPx.js +3 -0
- package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-BdZXnibG.js} +1 -1
- package/dist/assets/web/dist/assets/MetricsPage-o5fSaCTe.js +1 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-DHuWyjlq.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +1 -0
- package/dist/assets/web/dist/assets/PlanePage-DpWfiX4C.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-C9y3nbo8.js → ResidentDetailPage-DG86v39Y.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-i1RG9e7g.js → ResidentsIndexPage-x6p689VH.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-ysJBY8xQ.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-CuzFchcn.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-kHbbxVHx.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-DvYwM2TE.js → ScheduledPage-BuLmfcbG.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-BfID5nF4.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-Bo6yCyXZ.js → SettingsPage-BujWkdU_.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-CAfS1AUi.js → StatusDot-C8Bc0pTX.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-tZoum_T-.js → Tooltip-BWwJx27K.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BmdOHwNn.js → UnitRoutePage-B9kjA1AT.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-C8b5YTHf.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-C3f4WYNe.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-DSHxUJc3.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-BSGCCBP6.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-DFeYrMKk.js} +1 -1
- package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-L_QOnFVv.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-DLQWUwhu.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-1DlMmrEO.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-BTB6os6N.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BvJI2z3u.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-B11wGW_h.js} +1 -1
- package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-BiIWx5Pw.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-DfbEpHXR.js → dist-CpnyQGOb.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-nSglEXaD.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-x7o8qMl8.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DGrRyqTQ.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb--_dh58Nw.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-DbpUXwyg.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-BG0CuZG5.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-CQIl5qyR.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-9PJNQTcw.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-CzVTzjEI.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-BhCmAz3Q.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-BmI12COR.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-Cib-ISTS.js} +1 -1
- package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-D3lDapvI.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-DRPY8vOK.js} +1 -1
- package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-B9VttK6m.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-C0saNJYj.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-DABQtONT.js +1 -0
- package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-BCFbtYAj.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-nwBjnrhh.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja-CMYO9byN.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-Cko8HnE_.js} +1 -1
- package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-Dzv__YsX.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CTFxagih.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-_Xvten1-.js} +1 -1
- package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-bLps4qOu.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D6UaYsUP.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-CoDIeULa.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-B1h4aAzP.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-D6CG4hvX.js → marko-B5N5XcOw.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-uxzjQO3q.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-BMJxs2TK.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim-DqQ3-8p1.js} +1 -1
- package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DsY6LK59.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl--pq9iJFp.js} +1 -1
- package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-RHCMbsM2.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CFNq_FCp.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-C9Acx9E8.js} +1 -1
- package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-DxlU8FBd.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-CLJ2kKih.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CzmdGoEp.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-D9yuxBET.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-CIRcEvnD.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-LOl-uUPh.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-BX2_sZHY.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-4d-QvKkY.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-DhB16UGy.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-Cm1xTUBr.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-BsfcBJdu.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DmyMXfRC.js → sseReplay-IzTdD4-3.js} +5 -5
- package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-Dwat3BSw.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-P4rtxzAW.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-DTcsi8S8.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-D-kP0Tg5.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-D-4vuedK.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-jEEWYxsu.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-CFnlrVzT.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-D5rGrPjs.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-nYIQ35or.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-3-UX8dna.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CLIPIX03.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-Ci9CpkHU.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DL5pEYYk.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-bO80qikC.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-BPze60Ej.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-BQUSon_o.js} +1 -1
- package/dist/cli.js +36552 -33171
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BoKzYa4B.js +0 -2
- package/dist/assets/web/dist/assets/HomePage-BG_ok-K2.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-ChCQOLgZ.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-D3wVpzBa.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-B3IirUVi.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DiFmtGaJ.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BT0cPVRw.js +0 -1
- package/dist/assets/web/dist/assets/main-5Gm_1Gv8.js +0 -28
- package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
|
@@ -66,6 +66,18 @@
|
|
|
66
66
|
"script_name": "{{bot.script}}"
|
|
67
67
|
}
|
|
68
68
|
],
|
|
69
|
+
// {{#if metrics}}
|
|
70
|
+
// The run-metrics dataset (docs/reference/specs/run-metrics.md): where RunHistoryDO
|
|
71
|
+
// writes a turned-final run's point, bound only when the deployment profile names a
|
|
72
|
+
// dataset (`metrics.dataset`) — nothing is created ahead of the deploy; the platform
|
|
73
|
+
// creates the dataset on first write. Without it the Worker deploys without the
|
|
74
|
+
// binding and writes no points.
|
|
75
|
+
"analytics_engine_datasets": [{ "binding": "RUN_METRICS", "dataset": "{{metrics.dataset}}" }],
|
|
76
|
+
// The dataset's name beside its binding (rendered together): /healthz `features`
|
|
77
|
+
// says `runMetrics:<dataset>` from it, so the bot's boot probe can compare the
|
|
78
|
+
// name its own config carries with the one this deploy bound.
|
|
79
|
+
"vars": { "RUN_METRICS_DATASET": "{{metrics.dataset}}" },
|
|
80
|
+
// {{/if}}
|
|
69
81
|
// SQLite-backed DOs (durable across restarts — AGENTS.md invariant 6).
|
|
70
82
|
"migrations": [
|
|
71
83
|
{ "tag": "v1", "new_sqlite_classes": ["MemoryDO"] },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "The deployment profile: where THIS installation runs. Copy to deploy/profile.json and fill it in (or run `switchboard deploy init`). The account is your Cloudflare account id; every hostname must be under `zone`, a zone in that account, unless the Worker names its own `zone` (also in the account); `workers.bot` is the one required Worker — leave `memory`, `resident` or `sandbox` out and `deploy plan` has no step for them (a bot-only profile is a one-step plan; without `memory` the config is not pushed anywhere). The project's docs site is not a Worker of an installation: it is the project's website, deployed by the project's own CI from project.json, so there is no `docs` entry. `configSource` is where `deploy all` reads the bot's runtime config from before building the image — a path, `github://owner/repo/path@ref` (needs CONFIG_REPO_TOKEN), or `op://Vault/Item/field` (needs OP_SERVICE_ACCOUNT_TOKEN); `secretsSource` is where `secrets put` reads values from — a directory of <NAME> files (the default when absent) or `op://Vault/Item`. `images` is where the bot, resident and sandbox container images come from: `registry` — the release's published images, copied once per version into your account registry by `deploy all` itself (or `deploy images` ahead of it) over HTTPS and referenced from there — no Docker anywhere, a Cloudflare API token with Containers Edit in CLOUDFLARE_API_TOKEN for the copy (an installation deploying published images; what `init` writes from the published package); or `build` (the default when absent) — each Worker's Dockerfile, built by wrangler where `deploy all` runs (a checkout; the project's own production). `artifacts` (optional) names the R2 bucket a run's files move through (docs/reference/specs/execution.md item 20): with it the bot Worker binds the bucket and `deploy` creates it before the upload (the credential then needs Workers R2 Storage: Edit); the bot's runtime config `artifacts.r2.bucket` must say the same name. `deploy plan` reads this example when no profile exists; `deploy all` refuses it.",
|
|
2
|
+
"$comment": "The deployment profile: where THIS installation runs. Copy to deploy/profile.json and fill it in (or run `switchboard deploy init`). The account is your Cloudflare account id; every hostname must be under `zone`, a zone in that account, unless the Worker names its own `zone` (also in the account); `workers.bot` is the one required Worker — leave `memory`, `resident` or `sandbox` out and `deploy plan` has no step for them (a bot-only profile is a one-step plan; without `memory` the config is not pushed anywhere). The project's docs site is not a Worker of an installation: it is the project's website, deployed by the project's own CI from project.json, so there is no `docs` entry. `configSource` is where `deploy all` reads the bot's runtime config from before building the image — a path, `github://owner/repo/path@ref` (needs CONFIG_REPO_TOKEN), or `op://Vault/Item/field` (needs OP_SERVICE_ACCOUNT_TOKEN); `secretsSource` is where `secrets put` reads values from — a directory of <NAME> files (the default when absent) or `op://Vault/Item`. `images` is where the bot, resident and sandbox container images come from: `registry` — the release's published images, copied once per version into your account registry by `deploy all` itself (or `deploy images` ahead of it) over HTTPS and referenced from there — no Docker anywhere, a Cloudflare API token with Containers Edit in CLOUDFLARE_API_TOKEN for the copy (an installation deploying published images; what `init` writes from the published package); or `build` (the default when absent) — each Worker's Dockerfile, built by wrangler where `deploy all` runs (a checkout; the project's own production). `artifacts` (optional) names the R2 bucket a run's files move through (docs/reference/specs/execution.md item 20): with it the bot Worker binds the bucket and `deploy` creates it before the upload (the credential then needs Workers R2 Storage: Edit); the bot's runtime config `artifacts.r2.bucket` must say the same name. `metrics` (optional) names the Analytics Engine dataset every finished run's point is written to (docs/reference/specs/run-metrics.md): the state Worker's template then binds it as `RUN_METRICS` with the name beside it in `RUN_METRICS_DATASET` — nothing is created ahead of the deploy, the platform creates the dataset on first write — and the bot's runtime config `metrics.dataset` must say the same name (the bot warns at boot when the two differ). `deploy plan` reads this example when no profile exists; `deploy all` refuses it.",
|
|
3
3
|
"account": "00000000000000000000000000000000",
|
|
4
4
|
"zone": "example.com",
|
|
5
5
|
"workers": {
|
|
@@ -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
|
|
@@ -141,6 +152,14 @@ export const POLICY: readonly Rule[] = [
|
|
|
141
152
|
{ action: "config:write", resource: "config-scope", resourceKind: "thread", when: [grant("config:write")] },
|
|
142
153
|
// A user edits only their own scope.
|
|
143
154
|
{ action: "config:write", resource: "config-scope", resourceKind: "user", when: [IS_SELF] },
|
|
155
|
+
// The author binding's one trusted write (record 0062, authorization.md item
|
|
156
|
+
// 18): `config set user --github` binds a person to a GitHub login, so it is
|
|
157
|
+
// its own action — `identity:write`, held by `all` and by a named grants
|
|
158
|
+
// entry, never a baseline — not the channel-config right: a `config:write`
|
|
159
|
+
// holder could otherwise author commits as any unbound person. The is-self
|
|
160
|
+
// row above does not carry it: a person never binds themself (`config set me
|
|
161
|
+
// --github` is refused at the registry door before this table is asked).
|
|
162
|
+
{ action: "identity:write", resource: "config-scope", resourceKind: "user", when: [grant("identity:write")] },
|
|
144
163
|
|
|
145
164
|
// ── agents ───────────────────────────────────────────────────────────────
|
|
146
165
|
// `agent:run:*` covers every agent through wildcard coverage (grants.ts).
|
|
@@ -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
|
|
@@ -175,6 +183,9 @@ export const HOSTED_DEADLINE_MARGIN_MINUTES = 60;
|
|
|
175
183
|
* and the pause before a busy spawn is asked again. */
|
|
176
184
|
export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes: 5, busyRetryMinutes: 2 } as const;
|
|
177
185
|
|
|
186
|
+
/** The plane's table (docs/decisions/0064): how long a finished run stays on it. */
|
|
187
|
+
export const PLANE = { recentMinutes: 60 } as const;
|
|
188
|
+
|
|
178
189
|
/** The named amounts a lease holds back, in minutes. Each stands for a step
|
|
179
190
|
* every run or round pays: `provision` is attach and restore before the
|
|
180
191
|
* harness's clock starts; `writeUp` is the final answer after the loop ends;
|
|
@@ -461,3 +472,17 @@ export const DEFAULT_GRANT: Grant = { renewals: 0 };
|
|
|
461
472
|
* hand-over is now `renewals: 5` — the longest problem a person hands over
|
|
462
473
|
* in one message before a plan should carry it. */
|
|
463
474
|
export const GRANT_RENEWALS_MAX = 12;
|
|
475
|
+
|
|
476
|
+
// ---- the idle unit (record 0051: a pipeline idles instead of ending) ----
|
|
477
|
+
|
|
478
|
+
/** `ship.idleDays`' default: zero — today's endings byte for byte. It moves to
|
|
479
|
+
* seven only in the unit that lands after the baseline and the billing answer
|
|
480
|
+
* are recorded (record 0051's plan), never before. */
|
|
481
|
+
export const IDLE_DAYS_DEFAULT = 0;
|
|
482
|
+
/** The most days one idle may wait: the platform's own `waitForEvent` ceiling
|
|
483
|
+
* (a year), which the wait must pass explicitly — its default is 24 hours. */
|
|
484
|
+
export const IDLE_DAYS_MAX = 365;
|
|
485
|
+
/** The most wakes one idle answers before it expires: a bound on how long a
|
|
486
|
+
* busy thread can keep an instance parked (read by the wait, this plan's
|
|
487
|
+
* fifth unit). */
|
|
488
|
+
export const IDLE_WAKES_MAX = 100;
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// parent record the spawn route reads the requester from, and the names the
|
|
9
9
|
// routes decide on.
|
|
10
10
|
//
|
|
11
|
-
import type
|
|
11
|
+
import { IDLE_DAYS_MAX, type Grant, type GrantSource } from "../budgets.js";
|
|
12
12
|
import { isVerbosity, type Verbosity } from "../verbosity.js";
|
|
13
13
|
import { isAddressSeverity, type AddressSeverity, type AddressSeveritySource } from "../ship/coordinator.js";
|
|
14
|
+
import { isHandoffShape, type Handoff } from "../ship/handoff.js";
|
|
14
15
|
|
|
15
16
|
// A coordinator is a Workflow instance in the shim Worker whose children are
|
|
16
17
|
// ordinary `dispatch()` runs as the requesting user. It holds no credential of
|
|
@@ -329,6 +330,11 @@ export interface CoordinatorInstance {
|
|
|
329
330
|
* the unit threads it owns — the unit-ending report's asides and the
|
|
330
331
|
* segment lines are `verbose` material. Absent reads as `quiet`. */
|
|
331
332
|
verbosity?: Verbosity;
|
|
333
|
+
/** The idle flag (record 0051; agent-ship item 8): `ship.idleDays` as the
|
|
334
|
+
* ship fork resolved it (user > channel > org), written here beside the
|
|
335
|
+
* grant and answered by the plan route — above zero, an idling ending
|
|
336
|
+
* becomes `idle`; absent reads as zero, today's endings. */
|
|
337
|
+
idleDays?: number;
|
|
332
338
|
/** The pipeline's caps as the profile gate clipped them: the rounds cap and the wall clock per unit. */
|
|
333
339
|
caps?: { maxRounds: number; maxMinutes: number };
|
|
334
340
|
/** The status card in the requesting thread, when the channel has one — what
|
|
@@ -350,6 +356,24 @@ export interface UnitSegment {
|
|
|
350
356
|
at: number;
|
|
351
357
|
}
|
|
352
358
|
|
|
359
|
+
/** The longest `why` an idle carries: an ending kind's name, never prose —
|
|
360
|
+
* the route refuses a longer one at the door, the row validator at the store. */
|
|
361
|
+
export const IDLE_WHY_MAX = 64;
|
|
362
|
+
|
|
363
|
+
/** The idle on a unit's row (record 0051): what a continuation needs, and the
|
|
364
|
+
* wakes spent against `IDLE_WAKES_MAX`. */
|
|
365
|
+
export interface UnitIdle {
|
|
366
|
+
/** The old ending kind the idle stands in for (`wall_clock_cap`, `stopped`, `continued`, …). */
|
|
367
|
+
why: string;
|
|
368
|
+
at: number;
|
|
369
|
+
renewalsLeft: number;
|
|
370
|
+
from?: string;
|
|
371
|
+
runId?: string;
|
|
372
|
+
spendUsd: number | null;
|
|
373
|
+
handoff?: Handoff;
|
|
374
|
+
wakes: number;
|
|
375
|
+
}
|
|
376
|
+
|
|
353
377
|
/** One unit of the plan an instance runs (a task string is a generated plan of
|
|
354
378
|
* one unit, `U1`): its branch, the units it waits on, and — as the runner
|
|
355
379
|
* reaches it — its thread, its pull request, the round boundaries the card
|
|
@@ -398,6 +422,15 @@ export interface CoordinatorUnit {
|
|
|
398
422
|
* renews the same segment twice. `from` is the sha the segment continues
|
|
399
423
|
* from, `runId` the coding run whose write-up briefs it. */
|
|
400
424
|
segments?: UnitSegment[];
|
|
425
|
+
/** The unit idles (record 0051; run-history item 50): written by `unit-end`
|
|
426
|
+
* on an `idle` ending in place of `ending`, so the unit stays unfinished
|
|
427
|
+
* and keeps owning its thread. `why` is the old kind, `renewalsLeft` what
|
|
428
|
+
* the grant still holds (the wake spends one — this plan's fifth unit),
|
|
429
|
+
* `from` the head a continuation opens from, `runId` the last coding
|
|
430
|
+
* child's run (absent when none ran), `spendUsd` the session's dollars
|
|
431
|
+
* (null once any run's cost is unknown), `handoff` that child's lists, and
|
|
432
|
+
* `wakes` how many wakes this idle has answered — zero at the write. */
|
|
433
|
+
idle?: UnitIdle;
|
|
401
434
|
/** The round boundaries the coordinator reported, oldest first (the `ship_round`
|
|
402
435
|
* vocabulary). `gate` rides an approve the machine's severity check caught
|
|
403
436
|
* carrying a finding at or above the level in force ([agent-ship](../../../docs/reference/specs/agent-ship.md)
|
|
@@ -450,6 +483,11 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
|
|
|
450
483
|
if (r.caps !== undefined && !(isObject(r.caps) && isFinite(r.caps.maxRounds) && isFinite(r.caps.maxMinutes)))
|
|
451
484
|
return false;
|
|
452
485
|
if (r.card !== undefined && !(isObject(r.card) && isText(r.card.channel) && isText(r.card.ts))) return false;
|
|
486
|
+
if (
|
|
487
|
+
r.idleDays !== undefined &&
|
|
488
|
+
!(Number.isInteger(r.idleDays) && (r.idleDays as number) >= 0 && (r.idleDays as number) <= IDLE_DAYS_MAX)
|
|
489
|
+
)
|
|
490
|
+
return false;
|
|
453
491
|
if (!isOptionalText(r.runId) || !isOptionalText(r.label)) return false;
|
|
454
492
|
if (r.verbosity !== undefined && !isVerbosity(r.verbosity)) return false;
|
|
455
493
|
if (r.attempt !== undefined && !(Number.isInteger(r.attempt) && (r.attempt as number) >= 2)) return false;
|
|
@@ -464,6 +502,17 @@ const isRoundGate = (v: unknown): boolean =>
|
|
|
464
502
|
Array.isArray(v.findings) &&
|
|
465
503
|
v.findings.every((f) => typeof f === "string");
|
|
466
504
|
|
|
505
|
+
const isUnitIdle = (v: unknown): boolean =>
|
|
506
|
+
isObject(v) &&
|
|
507
|
+
isText(v.why, IDLE_WHY_MAX) &&
|
|
508
|
+
isFinite(v.at) &&
|
|
509
|
+
isCount(v.renewalsLeft) &&
|
|
510
|
+
(v.from === undefined || isText(v.from)) &&
|
|
511
|
+
(v.runId === undefined || isText(v.runId)) &&
|
|
512
|
+
(v.spendUsd === null || isFinite(v.spendUsd)) &&
|
|
513
|
+
(v.handoff === undefined || isHandoffShape(v.handoff)) &&
|
|
514
|
+
isCount(v.wakes);
|
|
515
|
+
|
|
467
516
|
const isSegment = (v: unknown): boolean =>
|
|
468
517
|
isObject(v) &&
|
|
469
518
|
typeof v.index === "number" &&
|
|
@@ -486,6 +535,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
486
535
|
if (r.resume !== undefined && !isResume(r.resume)) return false;
|
|
487
536
|
if (r.lastPush !== undefined && !isText(r.lastPush)) return false;
|
|
488
537
|
if (r.segments !== undefined && (!Array.isArray(r.segments) || !r.segments.every(isSegment))) return false;
|
|
538
|
+
if (r.idle !== undefined && !isUnitIdle(r.idle)) return false;
|
|
489
539
|
if (
|
|
490
540
|
!Array.isArray(r.rounds) ||
|
|
491
541
|
r.rounds.length > MAX_ROUNDS ||
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
// plan's re-issue. A task string's ship branch waits for a person. Node-free:
|
|
34
34
|
// the shim Worker imports this by relative path.
|
|
35
35
|
|
|
36
|
-
import { DEFAULT_GRANT, GRANT_RENEWALS_MAX, type Grant, type GrantSource } from "../budgets.js";
|
|
36
|
+
import { DEFAULT_GRANT, GRANT_RENEWALS_MAX, IDLE_DAYS_MAX, type Grant, type GrantSource } from "../budgets.js";
|
|
37
37
|
import { DEFAULT_VERBOSITY, isVerbosity, type Verbosity } from "../verbosity.js";
|
|
38
38
|
import {
|
|
39
39
|
applyReturn,
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
openPlanCursor,
|
|
43
43
|
openUnitPipeline,
|
|
44
44
|
readyUnits,
|
|
45
|
+
type PlanCursor,
|
|
46
|
+
type UnitStatus,
|
|
45
47
|
renderUnitReport,
|
|
46
48
|
settleUnit,
|
|
47
49
|
startUnit,
|
|
@@ -163,8 +165,18 @@ export function readBotAnswer(
|
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
/** The answers the bot itself calls a passing condition — the step is asked
|
|
166
|
-
* again, under the policy. Everything else, refusals included, is the machine's.
|
|
167
|
-
|
|
168
|
+
* again, under the policy. Everything else, refusals included, is the machine's.
|
|
169
|
+
* `queued` is the plane's hold on a spawn (record 0064, "The queue"): a `409`
|
|
170
|
+
* saying the child's admission waits on an event — the step re-asks until the
|
|
171
|
+
* child is admitted, and the instance is never failed over a full plane. */
|
|
172
|
+
const TRANSIENT = new Set([
|
|
173
|
+
"github_unavailable",
|
|
174
|
+
"no_channel",
|
|
175
|
+
"thread_failed",
|
|
176
|
+
"unit_not_started",
|
|
177
|
+
"not_host",
|
|
178
|
+
"queued",
|
|
179
|
+
]);
|
|
168
180
|
export function transientRefusal(answer: BotAnswer): string | undefined {
|
|
169
181
|
const { ok, error, message } = answer.body;
|
|
170
182
|
if (ok !== false || typeof error !== "string" || !TRANSIENT.has(error)) return undefined;
|
|
@@ -191,6 +203,8 @@ interface PlanFacts {
|
|
|
191
203
|
grantSource: GrantSource;
|
|
192
204
|
/** The request's verbosity as the plan route answers it (routing-and-config item 28): what the unit threads hear. */
|
|
193
205
|
verbosity: Verbosity;
|
|
206
|
+
/** The idle flag beside the grant (record 0051): above zero, an idling ending becomes `idle`. */
|
|
207
|
+
idleDays: number;
|
|
194
208
|
/** The instance's mark as the plan route answers it: a generated one-unit plan (a `plan` with no `path`). */
|
|
195
209
|
generated: boolean;
|
|
196
210
|
/** The runs page base the bot answered: the report links a child's write-up to its run page with it. */
|
|
@@ -220,6 +234,12 @@ function readGrant(raw: unknown): Grant {
|
|
|
220
234
|
return { renewals: raw.renewals, ...(typeof cap === "number" && cap > 0 ? { costCapUsd: cap } : {}) };
|
|
221
235
|
}
|
|
222
236
|
|
|
237
|
+
/** The idle flag as the plan route answers it (record 0051): an integer from 0
|
|
238
|
+
* to the module's cap; anything unreadable is 0 — nothing idles on a guess. */
|
|
239
|
+
function readIdleDays(raw: unknown): number {
|
|
240
|
+
return typeof raw === "number" && Number.isInteger(raw) && raw >= 0 && raw <= IDLE_DAYS_MAX ? raw : 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
223
243
|
function readPlan(a: BotAnswer): PlanFacts {
|
|
224
244
|
const b = a.body;
|
|
225
245
|
if (b.ok !== true) throw new UnreadableAnswer("plan", a, "not ok");
|
|
@@ -240,6 +260,7 @@ function readPlan(a: BotAnswer): PlanFacts {
|
|
|
240
260
|
grantSource:
|
|
241
261
|
b.grantSource === "run" || b.grantSource === "user" || b.grantSource === "channel" ? b.grantSource : "org",
|
|
242
262
|
verbosity: isVerbosity(b.verbosity) ? b.verbosity : DEFAULT_VERBOSITY,
|
|
263
|
+
idleDays: readIdleDays(b.idleDays),
|
|
243
264
|
generated: b.generated === true,
|
|
244
265
|
...(typeof b.runPageBase === "string" && b.runPageBase.length > 0 ? { runPageBase: b.runPageBase } : {}),
|
|
245
266
|
repo: b.repo,
|
|
@@ -369,6 +390,11 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
369
390
|
prNumber,
|
|
370
391
|
url,
|
|
371
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 } : {}),
|
|
372
398
|
...(typeof a.body.autoMergeEnabled === "boolean" ? { autoMergeEnabled: a.body.autoMergeEnabled } : {}),
|
|
373
399
|
...(isCommitChecks(a.body.checks) ? { checks: a.body.checks } : {}),
|
|
374
400
|
// The ready-state facts beside the checks (agent-ship item 9): the
|
|
@@ -563,6 +589,10 @@ async function perform(
|
|
|
563
589
|
await step.do(action.step, STEP_CONFIG, () =>
|
|
564
590
|
call(bot, "pr-check", {
|
|
565
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 } : {}),
|
|
566
596
|
...(action.recover !== undefined ? { recover: action.recover } : {}),
|
|
567
597
|
...(action.pr !== undefined ? { pr: action.pr } : {}),
|
|
568
598
|
}),
|
|
@@ -662,6 +692,7 @@ async function runUnit(
|
|
|
662
692
|
grant: plan.grant,
|
|
663
693
|
grantSource: plan.grantSource,
|
|
664
694
|
verbosity: plan.verbosity,
|
|
695
|
+
idleDays: plan.idleDays,
|
|
665
696
|
generated: plan.generated,
|
|
666
697
|
...(plan.runPageBase !== undefined ? { runPageBase: plan.runPageBase } : {}),
|
|
667
698
|
...(resume !== undefined ? { resume } : {}),
|
|
@@ -734,13 +765,28 @@ async function runUnit(
|
|
|
734
765
|
kind: note.ending.kind,
|
|
735
766
|
report: renderUnitReport(state, endFacts),
|
|
736
767
|
threadReport: renderUnitReport(state, endFacts, state.input.verbosity ?? DEFAULT_VERBOSITY),
|
|
768
|
+
// An idle ending carries its continuation facts (record 0051): the
|
|
769
|
+
// bot writes them on the row's `idle` in place of an ending, with
|
|
770
|
+
// the coding run id it already receives below.
|
|
771
|
+
...(note.ending.kind === "idle"
|
|
772
|
+
? {
|
|
773
|
+
why: note.ending.why,
|
|
774
|
+
renewalsLeft: note.ending.renewalsLeft,
|
|
775
|
+
...(note.ending.from !== undefined ? { from: note.ending.from } : {}),
|
|
776
|
+
spendUsd: note.ending.spendUsd,
|
|
777
|
+
...(note.ending.handoff !== undefined ? { handoff: note.ending.handoff } : {}),
|
|
778
|
+
}
|
|
779
|
+
: {}),
|
|
737
780
|
},
|
|
738
781
|
...(state.pr !== undefined ? { pr: state.pr } : {}),
|
|
739
782
|
// A review_pending ending names the child's own last push so the next
|
|
740
|
-
// attempt's pre-check can start at the review round (the row's lastPush)
|
|
783
|
+
// attempt's pre-check can start at the review round (the row's lastPush)
|
|
784
|
+
// — an idled one the same, off the idle's `from` (record 0051).
|
|
741
785
|
...(note.ending.kind === "review_pending" && note.ending.headSha !== undefined
|
|
742
786
|
? { headSha: note.ending.headSha }
|
|
743
|
-
:
|
|
787
|
+
: note.ending.kind === "idle" && note.ending.why === "review_pending" && note.ending.from !== undefined
|
|
788
|
+
? { headSha: note.ending.from }
|
|
789
|
+
: {}),
|
|
744
790
|
...(state.lastCodingRunId !== undefined ? { codingRunId: state.lastCodingRunId } : {}),
|
|
745
791
|
// A continued ending is a segment's end, not the unit's: the bot
|
|
746
792
|
// writes the renewal as a row keyed by the next segment's index
|
|
@@ -768,12 +814,9 @@ function blockedReport(unit: string, dep: string, depEnding: string): string {
|
|
|
768
814
|
return `⛔ Blocked: ${unit} waits on ${dep}, which ended ${depEnding}${person ? " — a person's merge" : ""}. Re-issue the plan naming the remaining units once it is ${person ? "merged" : "resolved"}.`;
|
|
769
815
|
}
|
|
770
816
|
|
|
771
|
-
/** The
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
answerOf("plan", await step.do("plan", STEP_CONFIG, () => call(bot, "plan", { parentInstanceId: instanceId }))),
|
|
775
|
-
);
|
|
776
|
-
const graph: PlanGraph = {
|
|
817
|
+
/** The graph as one plan answer carries it: the instance's unit rows, in the plan's order. */
|
|
818
|
+
function graphOf(plan: PlanFacts, instanceId: string): PlanGraph {
|
|
819
|
+
return {
|
|
777
820
|
planId: plan.planId ?? instanceId,
|
|
778
821
|
units: plan.units.map((u) => ({
|
|
779
822
|
id: u.unit,
|
|
@@ -783,6 +826,54 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
783
826
|
dependsOn: u.dependsOn,
|
|
784
827
|
})),
|
|
785
828
|
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** The cursor over a freshly read selection (the orchestration-plane plan's
|
|
832
|
+
* re-read requirement): a surviving unit keeps its standing, a row appended
|
|
833
|
+
* mid-walk joins pending — walked after the current unit — and a row gone from the selection
|
|
834
|
+
* was merged by hand: it is walked as merged with nothing run and nothing
|
|
835
|
+
* told (`gone`), a dependency on it counted satisfied like any dependency
|
|
836
|
+
* outside the selection. `blocked` is derived state, so it is recomputed over
|
|
837
|
+
* the fresh graph rather than carried — a blocked unit whose failing
|
|
838
|
+
* dependency left the selection is in play again. */
|
|
839
|
+
function rereadCursor(cursor: PlanCursor, fresh: PlanGraph): { cursor: PlanCursor; gone: string[] } {
|
|
840
|
+
const gone = cursor.order.filter((id) => !fresh.units.some((u) => u.id === id));
|
|
841
|
+
const order = fresh.units.map((u) => u.id);
|
|
842
|
+
const status: Record<string, UnitStatus> = {};
|
|
843
|
+
for (const id of order) {
|
|
844
|
+
const prior = cursor.status[id];
|
|
845
|
+
status[id] = prior === undefined || prior === "blocked" ? "pending" : prior;
|
|
846
|
+
}
|
|
847
|
+
let changed = true;
|
|
848
|
+
while (changed) {
|
|
849
|
+
changed = false;
|
|
850
|
+
for (const u of fresh.units) {
|
|
851
|
+
if (status[u.id] !== "pending") continue;
|
|
852
|
+
if (u.dependsOn.some((d) => d in status && (status[d] === "failed" || status[d] === "blocked"))) {
|
|
853
|
+
status[u.id] = "blocked";
|
|
854
|
+
changed = true;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return { cursor: { order, status }, gone };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** The plan: its units one at a time in dependency order, then the endings of the units it never reached. */
|
|
862
|
+
async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string): Promise<PlanRunSummary> {
|
|
863
|
+
// The selection is read at every unit boundary (the orchestration-plane plan): the first read opens
|
|
864
|
+
// the cursor, and each boundary's — its own durable step, `plan/<n>`, so a
|
|
865
|
+
// replay meets the same read — rebuilds it, so a later bot can append a unit
|
|
866
|
+
// to a live instance or drop one merged by hand without killing it.
|
|
867
|
+
let reads = 0;
|
|
868
|
+
const readSelection = async (): Promise<PlanFacts> => {
|
|
869
|
+
reads += 1;
|
|
870
|
+
const name = reads === 1 ? "plan" : `plan/${reads}`;
|
|
871
|
+
return readPlan(
|
|
872
|
+
answerOf("plan", await step.do(name, STEP_CONFIG, () => call(bot, "plan", { parentInstanceId: instanceId }))),
|
|
873
|
+
);
|
|
874
|
+
};
|
|
875
|
+
let plan = await readSelection();
|
|
876
|
+
let graph = graphOf(plan, instanceId);
|
|
786
877
|
let cursor = openPlanCursor(graph);
|
|
787
878
|
const endings: Record<string, string> = {};
|
|
788
879
|
for (;;) {
|
|
@@ -809,7 +900,16 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
809
900
|
endings[next] = ending.kind;
|
|
810
901
|
// A unit is done for its dependents when the base carries its scope: the
|
|
811
902
|
// runner's merge, or a scope that had already landed before the attempt.
|
|
903
|
+
// An `idle` ending settles the unit `failed` for now: nothing waits yet —
|
|
904
|
+
// the indexed wait and the wake land with the fifth unit of record 0051's
|
|
905
|
+
// plan — so the walk is unchanged until then and the flag ships at zero.
|
|
812
906
|
cursor = settleUnit(graph, cursor, next, isSettledDone(ending.kind) ? "done" : "failed");
|
|
907
|
+
// The unit boundary's re-read (the orchestration-plane plan): the fresh rows are the selection now.
|
|
908
|
+
plan = await readSelection();
|
|
909
|
+
graph = graphOf(plan, instanceId);
|
|
910
|
+
const reread = rereadCursor(cursor, graph);
|
|
911
|
+
cursor = reread.cursor;
|
|
912
|
+
for (const id of reread.gone) endings[id] ??= "merged";
|
|
813
913
|
}
|
|
814
914
|
// Blocked units, in the plan's order: each told its own ending, so the rows
|
|
815
915
|
// and the summary say why it never ran. Every blocked unit's ending is known
|