@coreplane/switchboard 1.251.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 +28 -2
- package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
- package/dist/assets/deploy/cloudflare/worker.ts +6 -3
- package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +223 -38
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
- package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
- package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
- package/dist/assets/deploy/profile.example.json +1 -1
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +3 -2
- package/dist/assets/project.json +13 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +5 -5
- package/dist/assets/src/core/authz/policy.ts +8 -0
- package/dist/assets/src/core/budgets.ts +39 -0
- package/dist/assets/src/core/coordinator/contract.ts +93 -1
- package/dist/assets/src/core/coordinator/driver.ts +236 -21
- package/dist/assets/src/core/drain.ts +50 -0
- package/dist/assets/src/core/memory/engine.ts +123 -4
- package/dist/assets/src/core/memory/scorer.ts +12 -4
- package/dist/assets/src/core/memory/types.ts +73 -12
- package/dist/assets/src/core/modelCard.ts +32 -4
- package/dist/assets/src/core/modelPricing.ts +111 -1
- package/dist/assets/src/core/modelProxy/usage.ts +88 -0
- package/dist/assets/src/core/modelRegistry.ts +15 -1
- package/dist/assets/src/core/refusal.ts +4 -7
- package/dist/assets/src/core/reviewVerdict.ts +4 -0
- package/dist/assets/src/core/runEvents.ts +82 -3
- package/dist/assets/src/core/runFriction.ts +8 -2
- package/dist/assets/src/core/runLedger/types.ts +11 -0
- package/dist/assets/src/core/runMetrics.ts +237 -0
- package/dist/assets/src/core/runRecord.ts +46 -0
- package/dist/assets/src/core/runUsage.ts +67 -13
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +34 -14
- package/dist/assets/src/core/ship/coordinator.ts +485 -55
- package/dist/assets/src/core/ship/renewal.ts +10 -5
- package/dist/assets/src/core/trace/attrs.ts +24 -0
- package/dist/assets/src/core/types.ts +5 -5
- package/dist/assets/src/core/verbosity.ts +48 -0
- package/dist/assets/src/deploy/liveGate.ts +40 -13
- package/dist/assets/src/deploy/profile.ts +16 -0
- package/dist/assets/src/deploy/restart.ts +11 -12
- package/dist/assets/src/execution/residentDepCache.ts +50 -1
- package/dist/assets/src/execution/residentDepsStore.ts +40 -2
- package/dist/assets/src/execution/residentRefresh.ts +55 -3
- package/dist/assets/src/execution/residentSteps.ts +4 -0
- package/dist/assets/src/execution/sandboxErrors.ts +8 -0
- package/dist/assets/web/dist/.vite/manifest.json +444 -430
- 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-DUXd-Sl-.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-DDhMhrI7.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-BnEoOnGQ.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.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-B_GgeJrb.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-BXX4R113.js → SettingsPage-CkYIhLUl.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-C0TUgnC1.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-CN5cZdl3.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.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/budgets-CbIyPAER.js +1 -0
- 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-BCVXeBJ9.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-BYo2EZKy.js +11 -0
- 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 +5623 -2455
- package/package.json +1 -1
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
- package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-9klVWhSF.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
- package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
- package/dist/assets/web/dist/assets/main-Dkcbtu3u.js +0 -28
- package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +0 -9
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The cold sandbox's process supervisor, run by tini (PID 1): keeps the
|
|
3
|
+
// container alive across a crash of the SDK's container server
|
|
4
|
+
// (/container-server/sandbox). That server calls process.exit(1) on any
|
|
5
|
+
// uncaught exception, and the base image runs it as tini's one child — so one
|
|
6
|
+
// uncaught error ended the whole container: /workspace gone, a detached pi
|
|
7
|
+
// with it, every later /exec a failure until the platform noticed. Started
|
|
8
|
+
// here as a child instead, the server's exit is a restart a second later; the
|
|
9
|
+
// disk, the detached processes and the Durable Object's view of a running
|
|
10
|
+
// container all survive, and the next command starts a fresh process on the
|
|
11
|
+
// new server (docs/reference/specs/execution.md item 21).
|
|
12
|
+
//
|
|
13
|
+
// A stop from the platform is SIGTERM, which tini forwards here: forwarded to
|
|
14
|
+
// the server, and nothing starts again — a stop that lands during the pause
|
|
15
|
+
// between two starts still ends here — and the server's own exit status
|
|
16
|
+
// becomes this process's (the shell convention, 128 + the signal's number,
|
|
17
|
+
// when a signal ended it). A terminal's INT is forwarded as TERM too: one
|
|
18
|
+
// stop path, whatever the signal. A server that cannot stay up —
|
|
19
|
+
// QUICK_EXIT_LIMIT exits in a row, each within QUICK_EXIT_SECS of starting —
|
|
20
|
+
// ends the container with the last status instead of looping forever.
|
|
21
|
+
//
|
|
22
|
+
// Dependency-free Node ESM on the Node the image ships; `supervise` takes its
|
|
23
|
+
// process factory, clock, sleep and signal source as arguments, so
|
|
24
|
+
// src/deploy/sandboxRuntimeSupervisor.test.ts drives it with a fake child and
|
|
25
|
+
// a fake clock — no real process anywhere in the tests.
|
|
26
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
27
|
+
import { constants } from "node:os";
|
|
28
|
+
import { pathToFileURL } from "node:url";
|
|
29
|
+
import { setTimeout as sleepFor } from "node:timers/promises";
|
|
30
|
+
|
|
31
|
+
/** The SDK's container server, unless SANDBOX_RUNTIME names another. */
|
|
32
|
+
export const DEFAULT_RUNTIME = "/container-server/sandbox";
|
|
33
|
+
/** An exit within this many seconds of starting counts as a quick exit. */
|
|
34
|
+
export const QUICK_EXIT_SECS = 10;
|
|
35
|
+
/** This many quick exits in a row end the supervisor with the last status. */
|
|
36
|
+
export const QUICK_EXIT_LIMIT = 5;
|
|
37
|
+
/** The pause between an exit and the next start. */
|
|
38
|
+
export const RESTART_PAUSE_MS = 1_000;
|
|
39
|
+
|
|
40
|
+
/** One child's end as a shell would report it: its own exit code; 128 plus
|
|
41
|
+
* the signal's number when a signal ended it; 127 when it never started. */
|
|
42
|
+
export function endCode(code, signal) {
|
|
43
|
+
if (typeof code === "number") return code;
|
|
44
|
+
if (signal) return 128 + (constants.signals[signal] ?? 0);
|
|
45
|
+
return 127;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Start the runtime and start it again whenever it exits, until a stop signal
|
|
50
|
+
* or the quick-exit limit ends the loop; resolves with the exit status the
|
|
51
|
+
* whole process should carry. Every effect is an injected dependency:
|
|
52
|
+
* `spawn(runtime, args)` returns the child (a `kill(signal)` method and
|
|
53
|
+
* `exit`/`error` events are all it needs), `now()` is the clock in
|
|
54
|
+
* milliseconds, `sleep(ms)` the pause, `log(line)` the stderr line, and
|
|
55
|
+
* `signals` the emitter whose SIGTERM/SIGINT mean stop.
|
|
56
|
+
*/
|
|
57
|
+
export async function supervise({ runtime, args, spawn, now, sleep, log, signals }) {
|
|
58
|
+
let child;
|
|
59
|
+
let stopping = false;
|
|
60
|
+
let quick = 0;
|
|
61
|
+
let code;
|
|
62
|
+
const stop = () => {
|
|
63
|
+
stopping = true;
|
|
64
|
+
// Always TERM, whichever signal the stop arrived as: the server has one
|
|
65
|
+
// shutdown path and a terminal's INT should take it too.
|
|
66
|
+
if (child) child.kill("SIGTERM");
|
|
67
|
+
};
|
|
68
|
+
signals.on("SIGTERM", stop);
|
|
69
|
+
signals.on("SIGINT", stop);
|
|
70
|
+
try {
|
|
71
|
+
for (;;) {
|
|
72
|
+
const started = now();
|
|
73
|
+
child = spawn(runtime, args);
|
|
74
|
+
const ended = new Promise((resolve) => {
|
|
75
|
+
child.once("exit", (c, s) => resolve(endCode(c, s)));
|
|
76
|
+
// A runtime that cannot start (the path is wrong) never emits `exit`;
|
|
77
|
+
// 127 is what a shell would say, and the quick-exit limit ends the loop.
|
|
78
|
+
child.once("error", () => resolve(endCode(null, null)));
|
|
79
|
+
});
|
|
80
|
+
// A stop that landed before this start found no child to forward to;
|
|
81
|
+
// the one it missed gets it here.
|
|
82
|
+
if (stopping) child.kill("SIGTERM");
|
|
83
|
+
code = await ended;
|
|
84
|
+
child = undefined;
|
|
85
|
+
if (stopping) return code;
|
|
86
|
+
quick = now() - started < QUICK_EXIT_SECS * 1000 ? quick + 1 : 0;
|
|
87
|
+
if (quick >= QUICK_EXIT_LIMIT) {
|
|
88
|
+
log(
|
|
89
|
+
`sandbox-runtime-supervisor: the runtime exited with status ${code}, ` +
|
|
90
|
+
`${quick} times in a row within ${QUICK_EXIT_SECS}s of starting; giving up`,
|
|
91
|
+
);
|
|
92
|
+
return code;
|
|
93
|
+
}
|
|
94
|
+
log(`sandbox-runtime-supervisor: the runtime exited with status ${code}; starting it again`);
|
|
95
|
+
await sleep(RESTART_PAUSE_MS);
|
|
96
|
+
// A stop that landed during the pause: the last status is the answer,
|
|
97
|
+
// and nothing starts again.
|
|
98
|
+
if (stopping) return code;
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
signals.removeListener("SIGTERM", stop);
|
|
102
|
+
signals.removeListener("SIGINT", stop);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Run only when executed directly (tini runs this file), not when imported by
|
|
107
|
+
// the tests.
|
|
108
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
109
|
+
const code = await supervise({
|
|
110
|
+
runtime: process.env.SANDBOX_RUNTIME || DEFAULT_RUNTIME,
|
|
111
|
+
args: process.argv.slice(2),
|
|
112
|
+
spawn: (runtime, args) => nodeSpawn(runtime, args, { stdio: "inherit" }),
|
|
113
|
+
now: () => Date.now(),
|
|
114
|
+
sleep: sleepFor,
|
|
115
|
+
log: (line) => process.stderr.write(line + "\n"),
|
|
116
|
+
signals: process,
|
|
117
|
+
});
|
|
118
|
+
process.exit(code);
|
|
119
|
+
}
|
|
@@ -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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"cli": "tsx src/cli.ts",
|
|
40
40
|
"verify": "npm run verify:root && npm run verify --workspaces --if-present && npm run check:site",
|
|
41
41
|
"verify:root": "npm run check:consistency && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:dist",
|
|
42
|
-
"check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run licenses:check && npm run docs:check && npm run pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run check:project-facts && npm run check:registry-drift && npm run agents:check && npm run clock:check && npm run screenshots:check",
|
|
42
|
+
"check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run check:deps-drift && npm run licenses:check && npm run docs:check && npm run pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run check:project-facts && npm run check:registry-drift && npm run agents:check && npm run clock:check && npm run screenshots:check",
|
|
43
43
|
"ci:gate": "node scripts/ci-gate.mjs",
|
|
44
44
|
"fix": "npm run docs:gen && npm run pr-title:gen && npm run agents:gen && npm run clock:gen && npm run hygiene:gen && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
|
|
45
45
|
"deploy:gen": "npm run --silent cli -- deploy init",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"skills:sync": "tsx scripts/skills-sync.ts",
|
|
66
66
|
"skills:check": "tsx scripts/skills-sync.ts --check",
|
|
67
67
|
"licenses:check": "node scripts/licenses-check.mjs",
|
|
68
|
+
"check:deps-drift": "node scripts/deps-drift-check.mjs",
|
|
68
69
|
"docs:gen": "tsx scripts/docs-gen.ts",
|
|
69
70
|
"docs:check": "tsx scripts/docs-gen.ts --check",
|
|
70
71
|
"specs:check": "node scripts/specs-check.mjs",
|
package/dist/assets/project.json
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"$comment": "What each root npm script does and when to run it — the source of the Commands table in AGENTS.md (npm run agents:gen). A script without an entry fails agents:check, so a new script cannot ship undocumented.",
|
|
34
34
|
"verify": {
|
|
35
35
|
"does": "The whole gate: every root check, every workspace's verify, the site check — exactly what CI runs.",
|
|
36
|
-
"when": "
|
|
36
|
+
"when": "Local sessions, before requesting review. ~4 min. A pipeline agent leaves it to CI."
|
|
37
37
|
},
|
|
38
38
|
"verify:root": {
|
|
39
39
|
"does": "The bot package's gate: consistency checks, typecheck, lint, format, tests, dist.",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
},
|
|
46
46
|
"test": {
|
|
47
47
|
"does": "The whole vitest suite (bot, web, the plain-Node Worker tests) from one entry, after `deploy:gen`.",
|
|
48
|
-
"when": "
|
|
48
|
+
"when": "Local sessions, before pushing; a pipeline agent runs the changed set — the full suite is CI's."
|
|
49
49
|
},
|
|
50
50
|
"typecheck": {
|
|
51
51
|
"does": "TypeScript over the bot and its scripts, no emit.",
|
|
52
|
-
"when": "After type-level changes; `verify:root` runs it."
|
|
52
|
+
"when": "After type-level changes; `verify:root` runs it. A pipeline agent scopes it to the touched tsconfig."
|
|
53
53
|
},
|
|
54
54
|
"lint": {
|
|
55
55
|
"does": "ESLint over the whole tree.",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"when": "After bumping either half of a pair."
|
|
101
101
|
},
|
|
102
102
|
"check:pr-title": {
|
|
103
|
-
"does": "Judges one PR title as the changelog line: grammar, type, scope,
|
|
103
|
+
"does": "Judges one PR title as the changelog line: grammar, type, scope, `!`.",
|
|
104
104
|
"when": "`-- \"feat(scope): …\"` before opening a PR; CI's `title` check."
|
|
105
105
|
},
|
|
106
106
|
"pr-title:gen": {
|
|
@@ -143,6 +143,10 @@
|
|
|
143
143
|
"does": "Every production dependency's license is on the allowlist.",
|
|
144
144
|
"when": "After adding a dependency."
|
|
145
145
|
},
|
|
146
|
+
"check:deps-drift": {
|
|
147
|
+
"does": "Installed node_modules match the lockfile: drift fails by name.",
|
|
148
|
+
"when": "Part of `check:consistency`."
|
|
149
|
+
},
|
|
146
150
|
"docs:gen": {
|
|
147
151
|
"does": "Writes the generated regions of the reference docs from the command registry.",
|
|
148
152
|
"when": "After changing a command, flag, route, or config key; part of `fix`."
|
|
@@ -160,12 +164,12 @@
|
|
|
160
164
|
"when": "`-- --changed origin/main...HEAD [--test-guard]` before review; `-- --require` fails on an uncovered path; `-- --json` for machines."
|
|
161
165
|
},
|
|
162
166
|
"decisions:check": {
|
|
163
|
-
"does": "Every record under `docs/decisions/` and `docs/plans/` has a valid `status`, a superseded one names its successor,
|
|
167
|
+
"does": "Every record under `docs/decisions/` and `docs/plans/` has a valid `status`, a superseded one names its successor, an accepted body only gains an appended `## Amended`.",
|
|
164
168
|
"when": "Part of `check:consistency`; a failing record is superseded or amended by appending, never edited."
|
|
165
169
|
},
|
|
166
170
|
"hygiene:check": {
|
|
167
171
|
"does": "The public tree's imprint (company, people, trackers, plan ids, ids, dates) equals the recorded list, which only shrinks.",
|
|
168
|
-
"when": "Part of `check:consistency`. New hit: rewrite the line or allow it
|
|
172
|
+
"when": "Part of `check:consistency`. New hit: rewrite the line or allow it in `scripts/public-hygiene.allow`."
|
|
169
173
|
},
|
|
170
174
|
"hygiene:gen": {
|
|
171
175
|
"does": "Records the tree's remaining imprint after a scrub; refuses growth unless `-- --force`.",
|
|
@@ -180,7 +184,7 @@
|
|
|
180
184
|
"when": "Part of `check:consistency`."
|
|
181
185
|
},
|
|
182
186
|
"clock:gen": {
|
|
183
|
-
"does": "Regenerates both clock allowlists
|
|
187
|
+
"does": "Regenerates both clock allowlists: wall-clock reads (empty) and duration literals outside `src/core/budgets.ts`.",
|
|
184
188
|
"when": "Part of `fix`."
|
|
185
189
|
},
|
|
186
190
|
"clock:check": {
|
|
@@ -217,14 +221,14 @@
|
|
|
217
221
|
},
|
|
218
222
|
"screenshots:gen": {
|
|
219
223
|
"does": "Renders the dashboard screenshots whose inputs changed, both themes, recording each surface's input hashes in `docs/public/screenshots/manifest/` (`--force`: all).",
|
|
220
|
-
"when": "After a `web/` or fixture change, once `screenshots:check` names it; needs `npx playwright-core install chromium`, so
|
|
224
|
+
"when": "After a `web/` or fixture change, once `screenshots:check` names it; needs `npx playwright-core install chromium`, so not in `fix`."
|
|
221
225
|
},
|
|
222
226
|
"screenshots:check": {
|
|
223
227
|
"does": "Each surface's inputs still hash to what its screenshots were rendered from — no browser.",
|
|
224
228
|
"when": "Part of `check:consistency`."
|
|
225
229
|
},
|
|
226
230
|
"load": {
|
|
227
|
-
"does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route|door`.",
|
|
231
|
+
"does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route|intake|door`.",
|
|
228
232
|
"when": "Capacity receipts (docs/reference/specs/load-harness.md)."
|
|
229
233
|
}
|
|
230
234
|
}
|
package/dist/assets/source.json
CHANGED
|
@@ -238,7 +238,7 @@ Keep notes with the \`notes\` tool: one short document, replaced whole each time
|
|
|
238
238
|
// project's most expensive checks first; the rule is the runner's to hold, not
|
|
239
239
|
// a line every requester remembers to paste. Stack-agnostic on purpose — the
|
|
240
240
|
// classes are by duration, the project's own scripts and CI say which is which.
|
|
241
|
-
export const CHECKS_BY_COST = `CHECKS BY COST — push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it — from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end.
|
|
241
|
+
export const CHECKS_BY_COST = `CHECKS BY COST — push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it — from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope and scoped to the changed set — the tests nearest your change, the touched project's typecheck, the changed files' formatting, never the whole tree: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. Every CI pipeline runs the tests, the types, the formatting and the full verification on your push, so you never run them again: you validate and fix your own change before pushing, at the changed-set scope. Passing the full test suite and the full typecheck is NOT part of your criteria: CI is that gate and the only place they run — on a shared machine they cost minutes that every other run pays for. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Beyond the changed set, use judgement about what this change needs rather than a checklist, fixing forward with further commits and pushes. Never start an operation whose expected duration does not fit the time you have left minus what a commit, a push and the description need: push what there is and say plainly what is unverified instead. At the wind-down note, commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS} The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
|
|
242
242
|
|
|
243
243
|
const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
|
|
244
244
|
|
|
@@ -256,7 +256,7 @@ Workflow for shipping a PR:
|
|
|
256
256
|
3. Implement the change. Match the surrounding code's style and conventions.
|
|
257
257
|
4. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change.
|
|
258
258
|
5. Commit with a clear message and push the branch — before any full suite, build or full verification.
|
|
259
|
-
6.
|
|
259
|
+
6. CI runs the full suite, the typecheck and the full verification on that push — you never run them yourself; read CI's result if it lands within your budget and fix forward with further commits and pushes.
|
|
260
260
|
7. Call the submit_pr_description tool with the typed description object (content contract below) — every time, bringing forward the context you gained while implementing. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with \`gh\` or any API call.
|
|
261
261
|
8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
|
|
262
262
|
|
|
@@ -302,7 +302,7 @@ Workflow for shipping a change:
|
|
|
302
302
|
2. Implement the change. Match the surrounding code's style and conventions.
|
|
303
303
|
3. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change (dependencies are already present).
|
|
304
304
|
4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
|
|
305
|
-
5.
|
|
305
|
+
5. CI runs the full suite, the typecheck and the full verification on that push — you never run them yourself; read CI's result if it lands within your budget and fix forward with further commits and pushes.
|
|
306
306
|
6. Call the \`diff_digest\` tool to get a distilled summary of your change — per-file churn, totals, and risky-file flags. It is a distilled summary, not the raw diff: use it to shape the description you submit next — which files the Tour must walk, what belongs in risks.
|
|
307
307
|
7. Call the submit_pr_description tool with the typed description object (content contract below) — every time. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with any API call.
|
|
308
308
|
8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
|
|
@@ -345,7 +345,7 @@ Workflow for shipping a change:
|
|
|
345
345
|
2. Implement the change. Match the surrounding code's style and conventions.
|
|
346
346
|
3. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change (dependencies are already present).
|
|
347
347
|
4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
|
|
348
|
-
5.
|
|
348
|
+
5. CI runs the full suite, the typecheck and the full verification on that push — you never run them yourself; read CI's result if it lands within your budget and fix forward with further commits and pushes.
|
|
349
349
|
6. Call the \`diff_digest\` tool to get a distilled summary of your change — per-file churn, totals, and risky-file flags. It is a distilled summary, not the raw diff: use it to shape the description you submit next — which files the Tour must walk, what belongs in risks.
|
|
350
350
|
7. Call the submit_pr_description tool with the typed description object (content contract below) — every time. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with \`gh\` or any API call.
|
|
351
351
|
8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
|
|
@@ -544,7 +544,7 @@ Your tools work without a workspace: the GitHub tools — \`github_repos\` (the
|
|
|
544
544
|
|
|
545
545
|
${statusCardRule('"Read the issue and its thread", "Post the comment"')} A one-step answer needs no checklist; post one when the request has steps the person would wait on.
|
|
546
546
|
|
|
547
|
-
You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web. Other Switchboard agents can, and a plain message reaches them by itself
|
|
547
|
+
You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web — and you cannot hand off, route, forward or start another run: never say you will hand off, route, forward or start anything, because you cannot start another agent's run. Other Switchboard agents can do those things, and a plain message reaches them by itself — but only a new top-level message in the channel reaches the router; a reply in this thread comes back to you. For a code change or a pull request, say what you found and that the change is not yours to make, and that asking for it in plain words in a new top-level message in the channel — not a reply in this thread — like "in acme/api: fix the failing login test" starts the agent that makes the change, opens the PR and loops review; the same for a PR review ("review <PR URL>") and a web-research question ("compare X and Y on the web"). Never hand back a command or an \`agent:…\` line for the person to type: describe the ask in their words. Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
|
|
548
548
|
|
|
549
549
|
// The explore agent (docs/reference/specs/agent-explore.md): a long, read-only
|
|
550
550
|
// investigation — "run our CI locally and validate the claims", "how long does
|
|
@@ -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).
|
|
@@ -28,6 +28,10 @@ export const minutesToMs = (minutes: number): number => minutes * MINUTE_MS;
|
|
|
28
28
|
* as a literal where it is used. */
|
|
29
29
|
export const DAY_MS = 24 * 60 * MINUTE_MS;
|
|
30
30
|
|
|
31
|
+
/** One calendar week in milliseconds: the bucket the live false-silence ratio
|
|
32
|
+
* is printed per (docs/reference/specs/load-harness.md item 20). */
|
|
33
|
+
export const WEEK_MS = 7 * DAY_MS;
|
|
34
|
+
|
|
31
35
|
/** How long the confirmation a routed write is offered as stays pending
|
|
32
36
|
* (docs/decisions/0044; docs/reference/specs/routing-and-config.md item 25):
|
|
33
37
|
* the connect ticket's ten minutes. The bot passes it to the config object,
|
|
@@ -98,6 +102,13 @@ export function intakeReceiptRetentionMs(catchUpWindowMs: number): number {
|
|
|
98
102
|
return Math.max(DAY_MS, window + minutesToMs(DRAIN.maxMinutes));
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
/** The live false-silence join's recovery window (docs/reference/specs/load-harness.md
|
|
106
|
+
* item 20; docs/decisions/0058): a `silent` intake receipt counts as a false
|
|
107
|
+
* silence when the same person mentions the bot in the same thread within
|
|
108
|
+
* this window — the mention is the ignored person's recovery move, so a
|
|
109
|
+
* prompt one bounds the ratio the gate is judged by. */
|
|
110
|
+
export const INTAKE_RECOVERY_WINDOW_MS = 10 * MINUTE_MS;
|
|
111
|
+
|
|
101
112
|
/** The presets that run the tool loop, and the one pipeline preset. */
|
|
102
113
|
export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
|
|
103
114
|
export type LoopPreset = (typeof LOOP_PRESETS)[number];
|
|
@@ -153,17 +164,31 @@ export const FLOORS: Readonly<Record<RoundKind, number>> = {
|
|
|
153
164
|
* when the remainder allows it. */
|
|
154
165
|
export const MERGE_WAIT_ASK_MINUTES = 60;
|
|
155
166
|
|
|
167
|
+
/** A hosted ship parent's deadline margin past the pipeline's wall clock
|
|
168
|
+
* (record 0060): the row's `state.hosting.until` is the hand-off time plus
|
|
169
|
+
* the instance's `caps.maxMinutes` plus this hour, absorbing the runner's own
|
|
170
|
+
* scheduling slack before a reclaim closes the row `interrupted`. */
|
|
171
|
+
export const HOSTED_DEADLINE_MARGIN_MINUTES = 60;
|
|
172
|
+
|
|
156
173
|
/** The ship runner's waits, in minutes: the margin a child's wait allows past
|
|
157
174
|
* its budget, the slice a wait is asked in, the merge door's re-ask cadence,
|
|
158
175
|
* and the pause before a busy spawn is asked again. */
|
|
159
176
|
export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes: 5, busyRetryMinutes: 2 } as const;
|
|
160
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
|
+
|
|
161
181
|
/** The named amounts a lease holds back, in minutes. Each stands for a step
|
|
162
182
|
* every run or round pays: `provision` is attach and restore before the
|
|
163
183
|
* harness's clock starts; `writeUp` is the final answer after the loop ends;
|
|
164
184
|
* `commandWriteUp` is what the last command leaves for that answer; `execCall`
|
|
165
185
|
* is the exec client's wait past a command's own budget; `bearerGrace` is how
|
|
166
186
|
* far past the lease the model bearer stays valid for the last call's tail. */
|
|
187
|
+
/** The stall signal's window in minutes (docs/reference/specs/live-view.md
|
|
188
|
+
* item 32): a run's pace is counted over the last this-many minutes, and a
|
|
189
|
+
* run with no tool call for a whole window reads as stalled. */
|
|
190
|
+
export const PACE_WINDOW_MINUTES = 5;
|
|
191
|
+
|
|
167
192
|
export const ALLOWANCES = {
|
|
168
193
|
provision: 3,
|
|
169
194
|
writeUp: 3,
|
|
@@ -439,3 +464,17 @@ export const DEFAULT_GRANT: Grant = { renewals: 0 };
|
|
|
439
464
|
* hand-over is now `renewals: 5` — the longest problem a person hands over
|
|
440
465
|
* in one message before a plan should carry it. */
|
|
441
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;
|
|
@@ -8,8 +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
|
+
import { isVerbosity, type Verbosity } from "../verbosity.js";
|
|
12
13
|
import { isAddressSeverity, type AddressSeverity, type AddressSeveritySource } from "../ship/coordinator.js";
|
|
14
|
+
import { isHandoffShape, type Handoff } from "../ship/handoff.js";
|
|
13
15
|
|
|
14
16
|
// A coordinator is a Workflow instance in the shim Worker whose children are
|
|
15
17
|
// ordinary `dispatch()` runs as the requesting user. It holds no credential of
|
|
@@ -81,6 +83,22 @@ export function runFinishedEventType(runId: string): string {
|
|
|
81
83
|
return `${RUN_FINISHED_EVENT_PREFIX}${runId}`;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
/** The deploy-roll signals a child's reattach path sends its parent
|
|
87
|
+
* (run-history item 47a): `child-interrupted-<runId>` — the child closed
|
|
88
|
+
* `interrupted` for a restart from its request, so the wait settles at once
|
|
89
|
+
* beside `run-finished-<runId>` and the round ends with the child's own
|
|
90
|
+
* reason; `child-resumed-<runId>` — the same run carries on after a roll, so
|
|
91
|
+
* the wait keeps waiting. Same alphabet as the finish event; the parent
|
|
92
|
+
* confirms either by `read-record` before it acts. */
|
|
93
|
+
export const CHILD_INTERRUPTED_EVENT_PREFIX = "child-interrupted-";
|
|
94
|
+
export function childInterruptedEventType(runId: string): string {
|
|
95
|
+
return `${CHILD_INTERRUPTED_EVENT_PREFIX}${runId}`;
|
|
96
|
+
}
|
|
97
|
+
export const CHILD_RESUMED_EVENT_PREFIX = "child-resumed-";
|
|
98
|
+
export function childResumedEventType(runId: string): string {
|
|
99
|
+
return `${CHILD_RESUMED_EVENT_PREFIX}${runId}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
/** A message into a thread an unfinished unit owns (record 0051's reply-as-event rule): one row
|
|
85
103
|
* of the unit's event list, appended by the dispatcher, folded into the
|
|
86
104
|
* unit's next coding spawn — or run as one fresh turn at the unit's end.
|
|
@@ -307,6 +325,16 @@ export interface CoordinatorInstance {
|
|
|
307
325
|
* decision reads it. */
|
|
308
326
|
grant?: Grant;
|
|
309
327
|
grantSource?: GrantSource;
|
|
328
|
+
/** The request's verbosity (routing-and-config item 28), resolved once by
|
|
329
|
+
* the ship fork and written here beside `merge`: what the runner says in
|
|
330
|
+
* the unit threads it owns — the unit-ending report's asides and the
|
|
331
|
+
* segment lines are `verbose` material. Absent reads as `quiet`. */
|
|
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;
|
|
310
338
|
/** The pipeline's caps as the profile gate clipped them: the rounds cap and the wall clock per unit. */
|
|
311
339
|
caps?: { maxRounds: number; maxMinutes: number };
|
|
312
340
|
/** The status card in the requesting thread, when the channel has one — what
|
|
@@ -328,6 +356,24 @@ export interface UnitSegment {
|
|
|
328
356
|
at: number;
|
|
329
357
|
}
|
|
330
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
|
+
|
|
331
377
|
/** One unit of the plan an instance runs (a task string is a generated plan of
|
|
332
378
|
* one unit, `U1`): its branch, the units it waits on, and — as the runner
|
|
333
379
|
* reaches it — its thread, its pull request, the round boundaries the card
|
|
@@ -376,6 +422,15 @@ export interface CoordinatorUnit {
|
|
|
376
422
|
* renews the same segment twice. `from` is the sha the segment continues
|
|
377
423
|
* from, `runId` the coding run whose write-up briefs it. */
|
|
378
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;
|
|
379
434
|
/** The round boundaries the coordinator reported, oldest first (the `ship_round`
|
|
380
435
|
* vocabulary). `gate` rides an approve the machine's severity check caught
|
|
381
436
|
* carrying a finding at or above the level in force ([agent-ship](../../../docs/reference/specs/agent-ship.md)
|
|
@@ -428,7 +483,13 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
|
|
|
428
483
|
if (r.caps !== undefined && !(isObject(r.caps) && isFinite(r.caps.maxRounds) && isFinite(r.caps.maxMinutes)))
|
|
429
484
|
return false;
|
|
430
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;
|
|
431
491
|
if (!isOptionalText(r.runId) || !isOptionalText(r.label)) return false;
|
|
492
|
+
if (r.verbosity !== undefined && !isVerbosity(r.verbosity)) return false;
|
|
432
493
|
if (r.attempt !== undefined && !(Number.isInteger(r.attempt) && (r.attempt as number) >= 2)) return false;
|
|
433
494
|
return true;
|
|
434
495
|
}
|
|
@@ -441,6 +502,17 @@ const isRoundGate = (v: unknown): boolean =>
|
|
|
441
502
|
Array.isArray(v.findings) &&
|
|
442
503
|
v.findings.every((f) => typeof f === "string");
|
|
443
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
|
+
|
|
444
516
|
const isSegment = (v: unknown): boolean =>
|
|
445
517
|
isObject(v) &&
|
|
446
518
|
typeof v.index === "number" &&
|
|
@@ -463,6 +535,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
463
535
|
if (r.resume !== undefined && !isResume(r.resume)) return false;
|
|
464
536
|
if (r.lastPush !== undefined && !isText(r.lastPush)) return false;
|
|
465
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;
|
|
466
539
|
if (
|
|
467
540
|
!Array.isArray(r.rounds) ||
|
|
468
541
|
r.rounds.length > MAX_ROUNDS ||
|
|
@@ -527,6 +600,25 @@ export async function sendChecksSettled(
|
|
|
527
600
|
}
|
|
528
601
|
}
|
|
529
602
|
|
|
603
|
+
/** One deploy-roll signal to the child's parent (run-history item 47a): best
|
|
604
|
+
* effort like `sendRunFinished` — a refusal is answered, never thrown; a lost
|
|
605
|
+
* send costs the wait a chunk, never the round. */
|
|
606
|
+
export async function sendChildSignal(
|
|
607
|
+
workflow: WorkflowSender | undefined,
|
|
608
|
+
signal: { runId: string; parentInstanceId: string; kind: "interrupted" | "resumed"; reason: string; at: number },
|
|
609
|
+
): Promise<RunFinishedSend> {
|
|
610
|
+
const { runId, parentInstanceId: instance, kind, reason, at } = signal;
|
|
611
|
+
if (!workflow) return { kind: "no-binding", instance };
|
|
612
|
+
const type = kind === "interrupted" ? childInterruptedEventType(runId) : childResumedEventType(runId);
|
|
613
|
+
try {
|
|
614
|
+
const handle = await workflow.get(instance);
|
|
615
|
+
await handle.sendEvent({ type, payload: { runId, kind, reason, at, parentInstanceId: instance } });
|
|
616
|
+
return { kind: "sent", instance, type };
|
|
617
|
+
} catch (err) {
|
|
618
|
+
return { kind: "failed", instance, type, reason: err instanceof Error ? err.message : String(err) };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
530
622
|
/** The one send per committed terminal record (run-history item 47). */
|
|
531
623
|
export async function sendRunFinished(
|
|
532
624
|
workflow: WorkflowSender | undefined,
|