@coreplane/switchboard 1.252.0 → 1.253.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 +24 -0
- package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +146 -26
- 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/policy.ts +8 -0
- package/dist/assets/src/core/budgets.ts +17 -0
- package/dist/assets/src/core/coordinator/contract.ts +51 -1
- package/dist/assets/src/core/coordinator/driver.ts +102 -11
- 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/runEvents.ts +31 -1
- package/dist/assets/src/core/runFriction.ts +1 -0
- package/dist/assets/src/core/runMetrics.ts +237 -0
- package/dist/assets/src/core/runRecord.ts +46 -0
- package/dist/assets/src/core/ship/contract.ts +8 -15
- package/dist/assets/src/core/ship/coordinator.ts +105 -2
- package/dist/assets/src/deploy/profile.ts +16 -0
- package/dist/assets/web/dist/.vite/manifest.json +440 -426
- package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-3ELQWM0r.js → DeliveryPage-C7jaEJip.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
- package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-ChCQOLgZ.js → PendingTurnRow-C-pUgyyX.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-C9y3nbo8.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-i1RG9e7g.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-DvYwM2TE.js → ScheduledPage-C8FYoZy5.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-Bo6yCyXZ.js → SettingsPage-CkYIhLUl.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-CAfS1AUi.js → StatusDot-C0TUgnC1.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-tZoum_T-.js → Tooltip-CN5cZdl3.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BmdOHwNn.js → UnitRoutePage-D_EtAl8r.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
- package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-DfbEpHXR.js → dist-B1Mf2JbM.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
- package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
- package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
- package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
- package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
- package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
- package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
- package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
- package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
- package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
- package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
- package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DmyMXfRC.js → sseReplay-BYo2EZKy.js} +4 -4
- package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
- package/dist/cli.js +19192 -17592
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-BG_ok-K2.js +0 -2
- 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
|
@@ -167,6 +167,12 @@ defaults:
|
|
|
167
167
|
# verbosity: verbose # hear every acknowledgement of what the bot does for you
|
|
168
168
|
# harness:
|
|
169
169
|
# coding: opencode
|
|
170
|
+
# # The person's GitHub author binding (record 0062; authorization.md item 18):
|
|
171
|
+
# # the login their commits are authored as. A bare login here is resolved to
|
|
172
|
+
# # { login, id } once at first use; `config set user --user <id> --github
|
|
173
|
+
# # <login>` (identity:write holders) stores the pair. Never set by the person
|
|
174
|
+
# # themselves; `config clear me` keeps it. One login/id binds one person.
|
|
175
|
+
# github: octocat
|
|
170
176
|
|
|
171
177
|
# Authorization (docs/reference/specs/authorization.md item 9, docs/reference/authorization.md):
|
|
172
178
|
# two blocks. `restrict` names what is CLOSED unless granted — an agent listed
|
|
@@ -388,6 +394,8 @@ workspaceDir: ./workspaces
|
|
|
388
394
|
# ship:
|
|
389
395
|
# maxRounds: 3 # review rounds per unit
|
|
390
396
|
# maxMinutes: 240 # the ship preset's wall-clock budget in minutes (the registry's default)
|
|
397
|
+
# idleDays: 0 # days a ship unit idles instead of ending (record 0051): 0–365, 0 = today's
|
|
398
|
+
# # endings; also per channel/user (`channels.<id>.ship.idleDays`), user > channel > org
|
|
391
399
|
|
|
392
400
|
# Which harness each preset's runs are driven by, deployment-wide
|
|
393
401
|
# (docs/reference/specs/harness.md item 8): a mapping of preset to a harness's
|
|
@@ -454,10 +462,16 @@ workspaceDir: ./workspaces
|
|
|
454
462
|
# `tool` (default) forces a call to the router's `route` tool, whose schema is
|
|
455
463
|
# the answer, so prose cannot occur; `text` is the one-JSON-object text contract
|
|
456
464
|
# 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): `off`
|
|
466
|
+
# (default) — the operator never runs; `shadow` — one operator turn per
|
|
467
|
+
# admitted chat event, ahead of stage A, its decision written beside the
|
|
468
|
+
# routed request in the run store and nothing run from it; `on` — the
|
|
469
|
+
# operator's decision is what runs. `shadow` runs even under `auto: false`.
|
|
457
470
|
# routing:
|
|
458
471
|
# auto: false
|
|
459
472
|
# model: anthropic/claude-haiku-4-5
|
|
460
473
|
# answer: text
|
|
474
|
+
# operator: shadow
|
|
461
475
|
|
|
462
476
|
# 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;
|
|
463
477
|
# docs/reference/specs/routing-and-config.md item 27). How an unmentioned reply
|
|
@@ -519,6 +533,16 @@ workspaceDir: ./workspaces
|
|
|
519
533
|
# cacheRead: 0.125 # never $0, on the costs page and on a run's own page
|
|
520
534
|
# cacheWrite: 0
|
|
521
535
|
|
|
536
|
+
# Run metrics (docs/reference/specs/run-metrics.md). Optional. The Analytics Engine
|
|
537
|
+
# dataset every finished run's point lands in — the SAME name the deployment
|
|
538
|
+
# 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). The reader
|
|
540
|
+
# over the dataset — `metrics trend` and the /metrics page, behind the `metrics`
|
|
541
|
+
# capability — is later work of the same decision; until it lands, only `dataset`
|
|
542
|
+
# is read, for that boot comparison.
|
|
543
|
+
# metrics:
|
|
544
|
+
# dataset: switchboard_runs
|
|
545
|
+
|
|
522
546
|
# Delivery indicators: GET /delivery and `delivery report` (docs/reference/specs/delivery.md).
|
|
523
547
|
# Optional. Needs a GitHub credential (the App triple or GH_TOKEN); read-only on GitHub.
|
|
524
548
|
# delivery:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RunMetricsPoint } from "../../src/core/runMetrics.ts";
|
|
2
|
+
|
|
3
|
+
// The run-metrics sink (docs/reference/specs/run-metrics.md): where the
|
|
4
|
+
// RunHistoryDO writes the one point of a run whose row turned final, after the
|
|
5
|
+
// record's commit. Two implementations behind one seam (AGENTS.md invariant 2):
|
|
6
|
+
// the Workers Analytics Engine dataset when the Worker is deployed with the
|
|
7
|
+
// optional `RUN_METRICS` binding, the null sink otherwise — so a Worker without
|
|
8
|
+
// the binding behaves byte-identically, the same shape as `SHIP_COORDINATOR?`.
|
|
9
|
+
// The write is advisory on the caller's side too: one try, one console.warn,
|
|
10
|
+
// never a failed put or finish.
|
|
11
|
+
|
|
12
|
+
export interface RunMetricsSink {
|
|
13
|
+
write(point: RunMetricsPoint): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The dataset the deploy bound: `writeDataPoint` is synchronous and does not
|
|
17
|
+
* throw on a platform-side drop; the caller's try covers a missing or
|
|
18
|
+
* misconfigured binding. */
|
|
19
|
+
export class AnalyticsEngineSink implements RunMetricsSink {
|
|
20
|
+
constructor(private readonly dataset: AnalyticsEngineDataset) {}
|
|
21
|
+
write(point: RunMetricsPoint): void {
|
|
22
|
+
this.dataset.writeDataPoint({ indexes: point.indexes, blobs: point.blobs, doubles: point.doubles });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** No binding: points are dropped — the off state. */
|
|
27
|
+
export class NullSink implements RunMetricsSink {
|
|
28
|
+
write(): void {
|
|
29
|
+
// nothing is written without the binding
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -119,6 +119,8 @@ import {
|
|
|
119
119
|
type McpTicketState,
|
|
120
120
|
type SealedCredential,
|
|
121
121
|
} from "../../src/mcp/registry.ts";
|
|
122
|
+
import { isRunMetricsPoint, pointTurnsFinal, type RunMetricsPoint } from "../../src/core/runMetrics.ts";
|
|
123
|
+
import { AnalyticsEngineSink, NullSink, type RunMetricsSink } from "./runMetricsSink.ts";
|
|
122
124
|
import { injectedBuildStamp } from "../../src/deploy/buildStamp.ts";
|
|
123
125
|
import { systemClock } from "../../src/core/trace/clock.ts";
|
|
124
126
|
import { createTracer } from "../../src/core/trace/tracer.ts";
|
|
@@ -148,7 +150,7 @@ const traceSinks = [workerLogSink((line) => console.log(line))];
|
|
|
148
150
|
//
|
|
149
151
|
// Route surface (JSON in/out; bearer MEMORY_TOKEN on everything but /healthz):
|
|
150
152
|
// POST /retrieve {scopeKey, query, limit} → {records: MemoryRecord[]}
|
|
151
|
-
// POST /write {scopeKey, records: MemoryCandidate[]} → {ok, inserted, deduped, superseded}
|
|
153
|
+
// POST /write {scopeKey, records: MemoryCandidate[]} → {ok, inserted, deduped, restated, superseded, evicted}
|
|
152
154
|
// POST /sweep {scopeKey, dryRun?} → {ok, swept} (+ ids under dryRun — the marked rows, flipped to `swept`)
|
|
153
155
|
// GET /healthz → {ok:true} (deploy wake ping; touches no DO)
|
|
154
156
|
// Scheduled-firing routes (the record behind the /runs Scheduled panel;
|
|
@@ -161,7 +163,7 @@ const traceSinks = [workerLogSink((line) => console.log(line))];
|
|
|
161
163
|
// store key, owning the retention policy. Same bearer; /runs/put has its own 2 MiB
|
|
162
164
|
// body fence (a record is budgeted to 1.5 MiB upstream), every other route
|
|
163
165
|
// keeps the 512 KB one.
|
|
164
|
-
// POST /runs/put {storeKey, record, policy?, policyUpdatedAt?} → {ok, retained, stored, rewritten}
|
|
166
|
+
// POST /runs/put {storeKey, record, policy?, policyUpdatedAt?, point?} → {ok, retained, stored, rewritten}
|
|
165
167
|
// POST /runs/get {storeKey, id} → {record: RunRecord | null} (unknown/expired: null, 200)
|
|
166
168
|
// POST /runs/list {storeKey, limit?, before?, beforeId?, sinceMs?, agent?, channel?, threadKey?, parentRunId?}
|
|
167
169
|
// → {items: RunListItem[], nextBefore?: {finishedAt, id}} (cursor = the last row's list key)
|
|
@@ -198,6 +200,14 @@ export interface Env {
|
|
|
198
200
|
* binding is a cross-script one, and the class must exist on the bot before
|
|
199
201
|
* the state Worker may name it), and a finish then commits with no event. */
|
|
200
202
|
SHIP_COORDINATOR?: Workflow;
|
|
203
|
+
/** The run-metrics dataset (docs/reference/specs/run-metrics.md): where the
|
|
204
|
+
* RunHistoryDO writes one point per run whose row turned final. Optional
|
|
205
|
+
* like SHIP_COORDINATOR: this Worker deploys without it and every answer is
|
|
206
|
+
* byte-identical — the NullSink swallows the points. */
|
|
207
|
+
RUN_METRICS?: AnalyticsEngineDataset;
|
|
208
|
+
/** The dataset's name, rendered beside the binding, so `/healthz` can answer
|
|
209
|
+
* `runMetrics:<dataset>` and the bot's boot probe can compare names. */
|
|
210
|
+
RUN_METRICS_DATASET?: string;
|
|
201
211
|
MEMORY_TOKEN?: string;
|
|
202
212
|
}
|
|
203
213
|
|
|
@@ -381,8 +391,8 @@ export class MemoryDO extends DurableObject<Env> {
|
|
|
381
391
|
scopeKey: string,
|
|
382
392
|
candidates: MemoryCandidate[],
|
|
383
393
|
cap: number = DEFAULT_SCOPE_CAP,
|
|
384
|
-
): Promise<{ inserted: number; deduped: number; superseded: number; evicted: number }> {
|
|
385
|
-
const counts = { inserted: 0, deduped: 0, superseded: 0, evicted: 0 };
|
|
394
|
+
): Promise<{ inserted: number; deduped: number; restated: number; superseded: number; evicted: number }> {
|
|
395
|
+
const counts = { inserted: 0, deduped: 0, restated: 0, superseded: 0, evicted: 0 };
|
|
386
396
|
if (candidates.length === 0) return counts;
|
|
387
397
|
this.ctx.storage.transactionSync(() => {
|
|
388
398
|
let seq = this.sql.exec<{ next: number }>(`SELECT COALESCE(MAX(seq), -1) + 1 AS next FROM records`).one().next;
|
|
@@ -400,18 +410,41 @@ export class MemoryDO extends DurableObject<Env> {
|
|
|
400
410
|
// own TRUTHINESS test: `supersedes: ""` passes validation but means NO
|
|
401
411
|
// supersede to the engine, so it must dedup against the norm pool —
|
|
402
412
|
// an `!== undefined` branch here would hand it an empty pool and
|
|
403
|
-
// insert a duplicate active row.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
413
|
+
// insert a duplicate active row. A `restates` id is looked up first
|
|
414
|
+
// (the restate target); when it misses — not active, or another
|
|
415
|
+
// scope's id, which this DO simply doesn't hold — the candidate takes
|
|
416
|
+
// today's pool, so planWrite falls through to dedup-or-insert.
|
|
417
|
+
const restatePool = cand.restates
|
|
418
|
+
? this.sql
|
|
419
|
+
.exec<Row>(`SELECT * FROM records WHERE id = ? AND status = 'active'`, cand.restates)
|
|
420
|
+
.toArray()
|
|
421
|
+
.map(toRecord)
|
|
422
|
+
: [];
|
|
423
|
+
const relevant =
|
|
424
|
+
restatePool.length > 0
|
|
425
|
+
? restatePool
|
|
426
|
+
: (cand.supersedes
|
|
427
|
+
? this.sql.exec<Row>(`SELECT * FROM records WHERE id = ? AND status = 'active'`, cand.supersedes)
|
|
428
|
+
: this.sql.exec<Row>(
|
|
429
|
+
`SELECT * FROM records WHERE status = 'active' AND norm = ? ORDER BY seq`,
|
|
430
|
+
normalizeText(cand.text),
|
|
431
|
+
)
|
|
410
432
|
)
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
.map(toRecord);
|
|
433
|
+
.toArray()
|
|
434
|
+
.map(toRecord);
|
|
414
435
|
const plan = planWrite(relevant, cand, (c) => mintRecord(scopeKey, seq++, now, c));
|
|
436
|
+
if (plan.action === "restate") {
|
|
437
|
+
// The shown record is refreshed in place; COALESCE keeps the stored
|
|
438
|
+
// confidence when the plan carries none (neither side had a value).
|
|
439
|
+
this.sql.exec(
|
|
440
|
+
`UPDATE records SET use_count = use_count + 1, last_used_at = ?, confidence = COALESCE(?, confidence) WHERE id = ?`,
|
|
441
|
+
now,
|
|
442
|
+
plan.confidence ?? null,
|
|
443
|
+
plan.target.id,
|
|
444
|
+
);
|
|
445
|
+
counts.restated++;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
415
448
|
if (plan.action === "dedup") {
|
|
416
449
|
this.sql.exec(`UPDATE records SET use_count = use_count + 1 WHERE id = ?`, plan.target.id);
|
|
417
450
|
counts.deduped++;
|
|
@@ -1485,10 +1518,15 @@ type HeartbeatAnswer = FenceResult & { stop?: StopMode | null; phase?: LivePhase
|
|
|
1485
1518
|
|
|
1486
1519
|
export class RunHistoryDO extends DurableObject<Env> {
|
|
1487
1520
|
private readonly sql: SqlStorage;
|
|
1521
|
+
/** Where a turned-final run's point goes (run-metrics.md): the Analytics
|
|
1522
|
+
* Engine dataset when the deploy bound one, the NullSink otherwise —
|
|
1523
|
+
* selected once at construction, the `SHIP_COORDINATOR?` shape. */
|
|
1524
|
+
private readonly metrics: RunMetricsSink;
|
|
1488
1525
|
|
|
1489
1526
|
constructor(ctx: DurableObjectState, env: Env) {
|
|
1490
1527
|
super(ctx, env);
|
|
1491
1528
|
this.sql = ctx.storage.sql;
|
|
1529
|
+
this.metrics = env.RUN_METRICS !== undefined ? new AnalyticsEngineSink(env.RUN_METRICS) : new NullSink();
|
|
1492
1530
|
// Idempotent schema. `runs` carries the listing columns plus the record
|
|
1493
1531
|
// minus its events as JSON (`summary_json`, what `list` returns); events
|
|
1494
1532
|
// live one per row keyed (run_id, seq) so a 5000-event run is paged, never
|
|
@@ -2063,8 +2101,10 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2063
2101
|
gen: string,
|
|
2064
2102
|
record: RunRecord,
|
|
2065
2103
|
proposal?: RunPolicyProposal,
|
|
2104
|
+
point?: RunMetricsPoint,
|
|
2066
2105
|
): Promise<FenceResult & { stored?: boolean; event?: RunFinishedSend["kind"] }> {
|
|
2067
2106
|
let out: FenceResult & { stored?: boolean } = { ok: true };
|
|
2107
|
+
let turnedFinal = false;
|
|
2068
2108
|
this.ctx.storage.transactionSync(() => {
|
|
2069
2109
|
const fence = checkFence(this.liveRow(runId), gen);
|
|
2070
2110
|
if (!fence.ok) {
|
|
@@ -2073,9 +2113,14 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2073
2113
|
}
|
|
2074
2114
|
const put = this.upsertInTransaction(record, proposal);
|
|
2075
2115
|
this.deleteLiveRows([runId]);
|
|
2116
|
+
turnedFinal = put.turnedFinal;
|
|
2076
2117
|
out = { ok: true, stored: put.stored };
|
|
2077
2118
|
});
|
|
2078
2119
|
if (!out.ok) return out;
|
|
2120
|
+
// The point after the commit, never inside it (`sendRunFinished`'s placement):
|
|
2121
|
+
// the finish usually replaces the start tombstone, so this is where most
|
|
2122
|
+
// runs are counted (run-metrics.md item 2).
|
|
2123
|
+
this.writeMetricsPoint(runId, point, turnedFinal && out.stored === true);
|
|
2079
2124
|
if ((await this.ctx.storage.getAlarm()) === null)
|
|
2080
2125
|
await this.ctx.storage.setAlarm(systemClock() + RUN_SWEEP_INTERVAL_MS);
|
|
2081
2126
|
await this.refreshSessionBytes(record.session?.key);
|
|
@@ -2338,15 +2383,21 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2338
2383
|
* when the stored version changed (`event_count`, `finished_at`, `bytes`) —
|
|
2339
2384
|
* an identical retry is a no-op on `run_events`. `stored: false` when the
|
|
2340
2385
|
* record itself fell outside the (possibly just-updated) policy: it was
|
|
2341
|
-
* written and deleted in the same transaction, so nothing of it remains.
|
|
2386
|
+
* written and deleted in the same transaction, so nothing of it remains.
|
|
2387
|
+
* `point` is the record's metrics point, computed by the client
|
|
2388
|
+
* (run-metrics.md): written to the sink AFTER the commit, only when the row
|
|
2389
|
+
* turned final and the record was stored — `turnedFinal` stays internal (the
|
|
2390
|
+
* route strips it), so the wire answer is exactly the shape it always was. */
|
|
2342
2391
|
async put(
|
|
2343
2392
|
record: RunRecord,
|
|
2344
2393
|
proposal?: RunPolicyProposal,
|
|
2345
|
-
|
|
2346
|
-
|
|
2394
|
+
point?: RunMetricsPoint,
|
|
2395
|
+
): Promise<{ ok: true; retained: number; stored: boolean; rewritten: boolean; turnedFinal: boolean }> {
|
|
2396
|
+
let result = { ok: true as const, retained: 0, stored: false, rewritten: false, turnedFinal: false };
|
|
2347
2397
|
this.ctx.storage.transactionSync(() => {
|
|
2348
2398
|
result = this.upsertInTransaction(record, proposal);
|
|
2349
2399
|
});
|
|
2400
|
+
this.writeMetricsPoint(record.id, point, result.turnedFinal && result.stored);
|
|
2350
2401
|
// A coordinator child closed OUTSIDE the ledger's finish — the run loop or
|
|
2351
2402
|
// a reclaim writing an `interrupted` record, the pi harness's typed restart,
|
|
2352
2403
|
// a resume abandoning a lost workspace — still wakes its parent's wait at
|
|
@@ -2370,7 +2421,7 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2370
2421
|
private upsertInTransaction(
|
|
2371
2422
|
record: RunRecord,
|
|
2372
2423
|
proposal?: RunPolicyProposal,
|
|
2373
|
-
): { ok: true; retained: number; stored: boolean; rewritten: boolean } {
|
|
2424
|
+
): { ok: true; retained: number; stored: boolean; rewritten: boolean; turnedFinal: boolean } {
|
|
2374
2425
|
{
|
|
2375
2426
|
const now = systemClock();
|
|
2376
2427
|
const policy = proposal ? this.applyProposal(proposal, now).policy : this.policyState().policy;
|
|
@@ -2387,11 +2438,29 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2387
2438
|
const { events, ...summary } = stored;
|
|
2388
2439
|
const bytes = utf8ByteLength(JSON.stringify(stored));
|
|
2389
2440
|
const existing = this.sql
|
|
2390
|
-
.exec<{ event_count: number; finished_at: number; bytes: number }>(
|
|
2391
|
-
`SELECT event_count, finished_at, bytes FROM runs WHERE run_id = ?`,
|
|
2441
|
+
.exec<{ event_count: number; finished_at: number; bytes: number; summary_json: string }>(
|
|
2442
|
+
`SELECT event_count, finished_at, bytes, summary_json FROM runs WHERE run_id = ?`,
|
|
2392
2443
|
record.id,
|
|
2393
2444
|
)
|
|
2394
2445
|
.toArray()[0];
|
|
2446
|
+
// The one field of the stored summary the emission rule reads (run-metrics.md
|
|
2447
|
+
// item 2): the row's `provisional`, parsed alone — never deserialized whole.
|
|
2448
|
+
const existingProvisional = existing !== undefined && summaryIsProvisional(existing.summary_json);
|
|
2449
|
+
const turnedFinal = pointTurnsFinal(
|
|
2450
|
+
existing !== undefined ? { provisional: existingProvisional } : undefined,
|
|
2451
|
+
stored,
|
|
2452
|
+
);
|
|
2453
|
+
// A provisional record never overwrites a final row (run-history.md item 27;
|
|
2454
|
+
// run-metrics.md item 3): a start tombstone sitting in retry backoff or a
|
|
2455
|
+
// drain upgrade racing a fast finish would otherwise land after the finish
|
|
2456
|
+
// record, overwrite it with `interrupted` — and let the run's point be
|
|
2457
|
+
// written twice when a later final write turned the row "final" again.
|
|
2458
|
+
// Answered as stored, with nothing written: the row, its events and the
|
|
2459
|
+
// sessions table stay exactly as the final write left them.
|
|
2460
|
+
if (stored.provisional === true && existing !== undefined && !existingProvisional) {
|
|
2461
|
+
const retained = this.sql.exec<{ n: number }>(`SELECT COUNT(*) AS n FROM runs`).one().n;
|
|
2462
|
+
return { ok: true as const, retained, stored: true, rewritten: false, turnedFinal: false };
|
|
2463
|
+
}
|
|
2395
2464
|
const unchanged =
|
|
2396
2465
|
existing !== undefined &&
|
|
2397
2466
|
sameStoredVersion(
|
|
@@ -2471,10 +2540,25 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2471
2540
|
retained: kept.size,
|
|
2472
2541
|
stored: kept.has(record.id),
|
|
2473
2542
|
rewritten: existing !== undefined && !unchanged,
|
|
2543
|
+
turnedFinal,
|
|
2474
2544
|
};
|
|
2475
2545
|
}
|
|
2476
2546
|
}
|
|
2477
2547
|
|
|
2548
|
+
/** One point per run whose row turned final, AFTER the commit (run-metrics.md
|
|
2549
|
+
* item 2) — advisory: a throwing sink leaves the answer exactly as a
|
|
2550
|
+
* recording one would, and says so in one warn line with the run id and the
|
|
2551
|
+
* error's constructor name, never the point's contents. */
|
|
2552
|
+
private writeMetricsPoint(runId: string, point: RunMetricsPoint | undefined, turnedFinal: boolean): void {
|
|
2553
|
+
if (point === undefined || !turnedFinal) return;
|
|
2554
|
+
try {
|
|
2555
|
+
this.metrics.write(point);
|
|
2556
|
+
} catch (err) {
|
|
2557
|
+
const kind = err instanceof Error ? err.constructor.name : "Error";
|
|
2558
|
+
console.warn(`[runs/metrics] ${runId} point not written: ${kind}`);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2478
2562
|
/** Remove a run and its events. Returns whether a run row existed. */
|
|
2479
2563
|
async delete(id: string): Promise<boolean> {
|
|
2480
2564
|
let deleted = false;
|
|
@@ -2924,6 +3008,17 @@ function parseSummary(row: RunRow): Omit<RunRecord, "events"> | null {
|
|
|
2924
3008
|
}
|
|
2925
3009
|
}
|
|
2926
3010
|
|
|
3011
|
+
/** The one field of a stored row's summary the emission rule reads: whether the
|
|
3012
|
+
* row is a provisional tombstone. Deliberately not a full `RunRecord` parse
|
|
3013
|
+
* (run-metrics.md item 2) — this runs inside every upsert's transaction. */
|
|
3014
|
+
function summaryIsProvisional(summaryJson: string): boolean {
|
|
3015
|
+
try {
|
|
3016
|
+
return (JSON.parse(summaryJson) as { provisional?: unknown }).provisional === true;
|
|
3017
|
+
} catch {
|
|
3018
|
+
return false;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
|
|
2927
3022
|
function parseRunId(v: unknown): Validated<string> {
|
|
2928
3023
|
if (typeof v !== "string" || !RUN_ID_PATTERN.test(v)) return invalid("id must match ^[A-Za-z0-9_-]{1,64}$");
|
|
2929
3024
|
return { ok: true, value: v };
|
|
@@ -2939,16 +3034,24 @@ function parsePositiveInt(v: unknown, name: string, max: number): Validated<numb
|
|
|
2939
3034
|
return { ok: true, value: v };
|
|
2940
3035
|
}
|
|
2941
3036
|
|
|
2942
|
-
function parseRunPut(
|
|
3037
|
+
function parseRunPut(
|
|
3038
|
+
body: unknown,
|
|
3039
|
+
): Validated<{ storeKey: string; record: RunRecord; proposal?: RunPolicyProposal; point?: RunMetricsPoint }> {
|
|
2943
3040
|
if (typeof body !== "object" || body === null) return invalid("body must be a JSON object");
|
|
2944
3041
|
const b = body as Record<string, unknown>;
|
|
2945
3042
|
const key = parseStoreKey(b);
|
|
2946
3043
|
if (!key.ok) return key;
|
|
2947
3044
|
if (!isRunRecord(b.record)) return invalid("record must be a RunRecord");
|
|
2948
|
-
const out: { storeKey: string; record: RunRecord; proposal?: RunPolicyProposal } = {
|
|
3045
|
+
const out: { storeKey: string; record: RunRecord; proposal?: RunPolicyProposal; point?: RunMetricsPoint } = {
|
|
2949
3046
|
storeKey: key.value,
|
|
2950
3047
|
record: b.record,
|
|
2951
3048
|
};
|
|
3049
|
+
// The record's metrics point (run-metrics.md item 1), validated at the door
|
|
3050
|
+
// like everything else that reaches storage.
|
|
3051
|
+
if (b.point !== undefined) {
|
|
3052
|
+
if (!isRunMetricsPoint(b.point)) return invalid("point must be a RunMetricsPoint");
|
|
3053
|
+
out.point = b.point;
|
|
3054
|
+
}
|
|
2952
3055
|
if (b.policy !== undefined) {
|
|
2953
3056
|
if (typeof b.policy !== "object" || b.policy === null) return invalid("policy must be an object");
|
|
2954
3057
|
const p = b.policy as Record<string, unknown>;
|
|
@@ -3259,6 +3362,11 @@ function parseCandidate(v: unknown, i: number): Validated<MemoryCandidate> {
|
|
|
3259
3362
|
return invalid(`${at}.supersedes must be a string`);
|
|
3260
3363
|
out.supersedes = c.supersedes;
|
|
3261
3364
|
}
|
|
3365
|
+
if (c.restates !== undefined) {
|
|
3366
|
+
if (typeof c.restates !== "string" || c.restates.length > MAX_KEY_CHARS)
|
|
3367
|
+
return invalid(`${at}.restates must be a string`);
|
|
3368
|
+
out.restates = c.restates;
|
|
3369
|
+
}
|
|
3262
3370
|
return { ok: true, value: out };
|
|
3263
3371
|
}
|
|
3264
3372
|
|
|
@@ -4302,7 +4410,7 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
|
|
|
4302
4410
|
const parsed = parseRunPut({ ...b, storeKey: key.value });
|
|
4303
4411
|
if (!parsed.ok) return json({ error: parsed.error }, 400);
|
|
4304
4412
|
if (parsed.value.record.id !== runId.value) return json({ error: "record.id must equal runId" }, 400);
|
|
4305
|
-
const r = await stub.finish(runId.value, g.value, parsed.value.record, parsed.value.proposal);
|
|
4413
|
+
const r = await stub.finish(runId.value, g.value, parsed.value.record, parsed.value.proposal, parsed.value.point);
|
|
4306
4414
|
console.log(
|
|
4307
4415
|
`[runs/finish] ${key.value} ${runId.value} ok=${r.ok}${r.ok ? ` stored=${r.stored} event=${r.event}` : ` ${r.reason}`}`,
|
|
4308
4416
|
);
|
|
@@ -4319,8 +4427,10 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
|
|
|
4319
4427
|
if (pathname === "/runs/put") {
|
|
4320
4428
|
const parsed = parseRunPut(body);
|
|
4321
4429
|
if (!parsed.ok) return json({ error: parsed.error }, 400);
|
|
4322
|
-
const { storeKey, record, proposal } = parsed.value;
|
|
4323
|
-
|
|
4430
|
+
const { storeKey, record, proposal, point } = parsed.value;
|
|
4431
|
+
// `turnedFinal` stays internal: the wire answer is exactly the shape it
|
|
4432
|
+
// always was, binding or no binding (run-metrics.md item 4).
|
|
4433
|
+
const { turnedFinal: _turnedFinal, ...result } = await stub(storeKey).put(record, proposal, point);
|
|
4324
4434
|
console.log(
|
|
4325
4435
|
`[runs/put] ${storeKey} <- ${record.id} (${record.storedEventCount} events, stored=${result.stored}, ${result.retained} retained)`,
|
|
4326
4436
|
);
|
|
@@ -4404,11 +4514,21 @@ interface Admission {
|
|
|
4404
4514
|
authorized: boolean;
|
|
4405
4515
|
}
|
|
4406
4516
|
|
|
4517
|
+
/** What this deploy carries, for the bot's boot probe: the fixed route set,
|
|
4518
|
+
* plus `runMetrics:<dataset>` when the deploy bound the Analytics Engine
|
|
4519
|
+
* dataset (run-metrics.md item 5) — the name from the `RUN_METRICS_DATASET`
|
|
4520
|
+
* var rendered beside the binding, so the probe can compare it to the bot's. */
|
|
4521
|
+
export function featuresOf(env: Pick<Env, "RUN_METRICS" | "RUN_METRICS_DATASET">): string[] {
|
|
4522
|
+
const features = ["memory", "schedules", "runs", "config", "delivery", "costs"];
|
|
4523
|
+
if (env.RUN_METRICS !== undefined) features.push(`runMetrics:${env.RUN_METRICS_DATASET ?? "unknown"}`);
|
|
4524
|
+
return features;
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4407
4527
|
/** Every request, once `fetch` has decided whether it gets a root. */
|
|
4408
4528
|
async function handleRequest(request: Request, env: Env, admission: Admission): Promise<Response> {
|
|
4409
4529
|
const url = new URL(request.url);
|
|
4410
4530
|
if (url.pathname === "/healthz" && request.method === "GET")
|
|
4411
|
-
return json({ ok: true, build: BUILD, features:
|
|
4531
|
+
return json({ ok: true, build: BUILD, features: featuresOf(env) });
|
|
4412
4532
|
if (!admission.known) return json({ error: "not found" }, 404);
|
|
4413
4533
|
if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
4414
4534
|
if (!admission.authorized) return json({ error: "unauthorized" }, 401);
|
|
@@ -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.253.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.253.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.253.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.253.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
|
@@ -141,6 +141,14 @@ export const POLICY: readonly Rule[] = [
|
|
|
141
141
|
{ action: "config:write", resource: "config-scope", resourceKind: "thread", when: [grant("config:write")] },
|
|
142
142
|
// A user edits only their own scope.
|
|
143
143
|
{ action: "config:write", resource: "config-scope", resourceKind: "user", when: [IS_SELF] },
|
|
144
|
+
// The author binding's one trusted write (record 0062, authorization.md item
|
|
145
|
+
// 18): `config set user --github` binds a person to a GitHub login, so it is
|
|
146
|
+
// its own action — `identity:write`, held by `all` and by a named grants
|
|
147
|
+
// entry, never a baseline — not the channel-config right: a `config:write`
|
|
148
|
+
// holder could otherwise author commits as any unbound person. The is-self
|
|
149
|
+
// row above does not carry it: a person never binds themself (`config set me
|
|
150
|
+
// --github` is refused at the registry door before this table is asked).
|
|
151
|
+
{ action: "identity:write", resource: "config-scope", resourceKind: "user", when: [grant("identity:write")] },
|
|
144
152
|
|
|
145
153
|
// ── agents ───────────────────────────────────────────────────────────────
|
|
146
154
|
// `agent:run:*` covers every agent through wildcard coverage (grants.ts).
|
|
@@ -175,6 +175,9 @@ export const HOSTED_DEADLINE_MARGIN_MINUTES = 60;
|
|
|
175
175
|
* and the pause before a busy spawn is asked again. */
|
|
176
176
|
export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes: 5, busyRetryMinutes: 2 } as const;
|
|
177
177
|
|
|
178
|
+
/** The plane's table (docs/decisions/0064): how long a finished run stays on it. */
|
|
179
|
+
export const PLANE = { recentMinutes: 60 } as const;
|
|
180
|
+
|
|
178
181
|
/** The named amounts a lease holds back, in minutes. Each stands for a step
|
|
179
182
|
* every run or round pays: `provision` is attach and restore before the
|
|
180
183
|
* harness's clock starts; `writeUp` is the final answer after the loop ends;
|
|
@@ -461,3 +464,17 @@ export const DEFAULT_GRANT: Grant = { renewals: 0 };
|
|
|
461
464
|
* hand-over is now `renewals: 5` — the longest problem a person hands over
|
|
462
465
|
* in one message before a plan should carry it. */
|
|
463
466
|
export const GRANT_RENEWALS_MAX = 12;
|
|
467
|
+
|
|
468
|
+
// ---- the idle unit (record 0051: a pipeline idles instead of ending) ----
|
|
469
|
+
|
|
470
|
+
/** `ship.idleDays`' default: zero — today's endings byte for byte. It moves to
|
|
471
|
+
* seven only in the unit that lands after the baseline and the billing answer
|
|
472
|
+
* are recorded (record 0051's plan), never before. */
|
|
473
|
+
export const IDLE_DAYS_DEFAULT = 0;
|
|
474
|
+
/** The most days one idle may wait: the platform's own `waitForEvent` ceiling
|
|
475
|
+
* (a year), which the wait must pass explicitly — its default is 24 hours. */
|
|
476
|
+
export const IDLE_DAYS_MAX = 365;
|
|
477
|
+
/** The most wakes one idle answers before it expires: a bound on how long a
|
|
478
|
+
* busy thread can keep an instance parked (read by the wait, this plan's
|
|
479
|
+
* fifth unit). */
|
|
480
|
+
export const IDLE_WAKES_MAX = 100;
|