@coreplane/switchboard 1.244.0 → 1.245.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 +6 -3
- package/dist/assets/deploy/cloudflare/worker.ts +2 -0
- package/dist/assets/deploy/cloudflare-resident/threadErr.ts +337 -0
- package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +31 -262
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +5 -10
- package/dist/assets/deploy/secrets.manifest.json +12 -0
- 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/agents/registry.ts +4 -4
- package/dist/assets/src/config/profile.ts +17 -1
- package/dist/assets/src/core/authz/types.ts +8 -0
- package/dist/assets/src/core/budgets.ts +25 -0
- package/dist/assets/src/core/reviewVerdict.ts +101 -24
- package/dist/assets/src/core/runEvents.ts +26 -1
- package/dist/assets/src/core/runRecord.ts +34 -0
- package/dist/assets/src/core/ship/coordinator.ts +25 -21
- package/dist/assets/src/execution/residentRefresh.ts +25 -6
- package/dist/assets/web/dist/.vite/manifest.json +481 -462
- package/dist/assets/web/dist/assets/AppShell-qqs_p96-.js +1 -0
- package/dist/assets/web/dist/assets/CostsPage-Dd9aXxxI.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-VLTFopnl.js +2 -0
- package/dist/assets/web/dist/assets/InputMenu-lQP3ia9D.js +1 -0
- package/dist/assets/web/dist/assets/NotFoundPage-8ViskpN3.js +1 -0
- package/dist/assets/web/dist/assets/PendingTurnRow-D2gHef5v.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-nWLN8BOd.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-D3Q48CJW.js +1 -0
- package/dist/assets/web/dist/assets/{RunFoldRow-BPMQxTcl.js → RunFoldRow-CHy_IfZG.js} +1 -1
- package/dist/assets/web/dist/assets/RunRoutePage-C6kX5om4.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-WyeG5tKb.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-B8D5AU4d.js +1 -0
- package/dist/assets/web/dist/assets/SettingSelect-UTHsqUUS.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-BoW7Zjzt.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-BGHureCJ.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-CiP8qPd7.js +1 -0
- package/dist/assets/web/dist/assets/UnitRoutePage-2zEeRBP1.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Dail07rr.js → angular-html-qXhb9ti8.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BReExio8.js → angular-ts-DzvdyoUZ.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DhNO6Yyx.js → apl-Ck7Ru2Sm.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-BYotTY4C.js → astro-CwehvReb.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-mUxTtD1X.js → blade-WSyVbSFK.js} +1 -1
- package/dist/assets/web/dist/assets/{c-DhuiwcSQ.js → c-DMHI1wDz.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-GD2vFtRi.js → chapel-BF8AYK8_.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BhFDwkes.js → cobol-UAfImuWu.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-DWUgbE0r.js → coffee-D54hi96d.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CO9XFjGW.js → cpp-BRWfLgBc.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-B9pVurpn.js → crystal-CSqC5deT.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CFVVV6lI.js → css-D4ZuRFGk.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BtPs2AkQ.js → dist-73ZMn_Mg.js} +2 -2
- package/dist/assets/web/dist/assets/{durationTone-BobbycC-.js → durationTone-D51enaGR.js} +1 -1
- package/dist/assets/web/dist/assets/{edge-BDhIDQIb.js → edge-DDPtNbZc.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-DG3J8WoD.js → elixir-BTQkQfsJ.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-FiOdiaOE.js → elm-B_PkLzGZ.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-vxbtW503.js → erb-DfjIMVM_.js} +1 -1
- package/dist/assets/web/dist/assets/{eventSource-tRprhqm7.js → eventSource-By0B_w4S.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DINz8M3H.js → git-rebase-DP8wmn7K.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-yann6r9i.js → glimmer-js-G12VK2gK.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CXTbrZn_.js → glimmer-ts-D3XiSI8E.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-DNYC-hq0.js → glsl-DOKAhMj-.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-CwzhHzi9.js → graphql-CJB_fmON.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BBXF1cwR.js → hack-BHqtRWap.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-oEqBcsTy.js → haml-DwotyarS.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-YE5thkT1.js → handlebars-B6fDaA3U.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DCT1GYtQ.js → html-derivative-C5bpQmmI.js} +1 -1
- package/dist/assets/web/dist/assets/{html-Dp4jRQ1j.js → html-s52taj36.js} +1 -1
- package/dist/assets/web/dist/assets/{http-BPcGbmzJ.js → http-99zC935r.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CQnNHwyd.js → hurl-Bgk-4-0x.js} +1 -1
- package/dist/assets/web/dist/assets/{java-Dm8Ovj6W.js → java-CuV_c5I0.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Btm9oJK9.js → javascript-CZlOvSMQ.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-VZYjWF_U.js → jinja-BuXeZ4Tu.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-Dcmjxa9x.js → jison-DIuP0hkG.js} +1 -1
- package/dist/assets/web/dist/assets/{json-DoWiOGZ8.js → json-CZ74vfoD.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-Dvmo55ma.js → jsx-BNjRs8Vj.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-d8AtfV26.js → julia-OCE-fFOg.js} +1 -1
- package/dist/assets/web/dist/assets/{just-Cc5EStA-.js → just-CtvD87WF.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-B5fKKIH7.js → latex-C3kf86OR.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-DUYI9JND.js → liquid-B1WI--Kp.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-BhMwKWpw.js → lua-DtoT0gKg.js} +1 -1
- package/dist/assets/web/dist/assets/main-ClluP-Qf.js +28 -0
- package/dist/assets/web/dist/assets/main-CpkwzHX6.css +1 -0
- package/dist/assets/web/dist/assets/{marko-Cnf2cPAi.js → marko-BE2MC6Yt.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-FDDXoir9.js → mdc-BwO4weXJ.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-q-8U_1jR.js → nginx-BTuOgGVC.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DIA0TUWs.js → nim-CiIUM-oV.js} +1 -1
- package/dist/assets/web/dist/assets/{org-CR88hy7S.js → org-L78DEWB-.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-Bkd8N7ja.js → perl-KqHOm4Ft.js} +1 -1
- package/dist/assets/web/dist/assets/{php-BOs84mvy.js → php-C41XbEkn.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-BbpZBb3w.js → pug-B12-4i-k.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-D2jQpjFE.js → qml--fw2Up5c.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DKzcAQ22.js → r-Db1JBXrC.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-SOQMWcC9.js → razor-BYpVBgbu.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-B6KK4W8d.js → regexp-BMqGLSPc.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-kPSg-NPf.js → rst-B63n7-Sa.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cKS-JYLH.js → ruby-D3VI8UYv.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-DNxzE5gs.js → sas-DxRf8ZAR.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-DGcHQ5OO.js → scss-DRRA6d6A.js} +1 -1
- package/dist/assets/web/dist/assets/seed-DFfT5S_N.js +6 -0
- package/dist/assets/web/dist/assets/{settingsApi-priOPiwu.js → settingsApi-Dm6eaQcu.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CRZeZBa5.js → shellscript-CoOBe3_y.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-DSNEL0HX.js → shellsession-DOV1HYxf.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-zisNbkDO.js → soy-Acpmpp1W.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-MG06KMMb.js → sql-DeT9N-fA.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DaRtc7nI.js → sseReplay-DPXVxhL5.js} +3 -3
- package/dist/assets/web/dist/assets/{stata-CB_wKwv9.js → stata-CbxXAMO4.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-DYdSt7re.js → surrealql-BdKbi4hc.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DHO-F7c7.js → svelte-tAcjDM7R.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CUL5L4Lr.js → templ-CObINNJj.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-_JNEKjVW.js → tex-BYwpZT1J.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags--9EpEV_8.js → ts-tags-BciREL5R.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-BcNq2x2u.js → tsx-CRiv3Pr3.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-EsY5Dk_D.js → twig-DuZZ1Ej0.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-CyA_HiUH.js → typescript-jkHz_5HZ.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-SzaHlDt4.js → typst-DOac4Gal.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-D3cn5acV.js → vue-CgOX5wXA.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-6MxhUxOa.js → vue-html-Duu99VNw.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-qJnqhgo9.js → vue-vine-Bmj_wvRJ.js} +1 -1
- package/dist/assets/web/dist/assets/wallClock-DD_fPuSY.js +1 -0
- package/dist/assets/web/dist/assets/{xml-DyRpC7xv.js → xml-BtUIVeWH.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BXMRPAsg.js → xsl-wX71n1E5.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BNmPCtAN.js → yaml-CdLdXvmz.js} +1 -1
- package/dist/cli.js +1474 -684
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-Ck4hSZDi.js +0 -1
- package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +0 -2
- package/dist/assets/web/dist/assets/NotFoundPage-CiGrVOyv.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-C5USV_8P.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +0 -1
- package/dist/assets/web/dist/assets/RunsTabs-BWRHzhtT.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +0 -1
- package/dist/assets/web/dist/assets/StatusDot-_Q_bmReh.js +0 -1
- package/dist/assets/web/dist/assets/Tooltip-Cafsgd4m.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +0 -1
- package/dist/assets/web/dist/assets/main-C4366PZF.css +0 -1
- package/dist/assets/web/dist/assets/main-DlTNAC6c.js +0 -28
- package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +0 -6
- package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +0 -1
|
@@ -81,7 +81,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
81
81
|
import type { DirectoryBackup, SandboxCommand } from "@cloudflare/sandbox";
|
|
82
82
|
import { createExtensionProcessSandbox } from "@cloudflare/sandbox/extensions";
|
|
83
83
|
import { DurableObject } from "cloudflare:workers";
|
|
84
|
-
import { BASH_TIMEOUT_MAX_MS,
|
|
84
|
+
import { BASH_TIMEOUT_MAX_MS, clampBashTimeout } from "../../src/execution/bashTimeout.js";
|
|
85
85
|
import { selectBindingsToPurge } from "../../src/execution/bindingPurge.js";
|
|
86
86
|
import { busyAfterKillReason, planForceDetach } from "../../src/execution/residentDetach.js";
|
|
87
87
|
import { parseReadonly, planReadonlyAttach } from "../../src/execution/residentReadonly.js";
|
|
@@ -323,6 +323,16 @@ import {
|
|
|
323
323
|
} from "../../src/execution/residentDepsStore.js";
|
|
324
324
|
import { buildId, injectedBuildStamp } from "../../src/deploy/buildStamp.js";
|
|
325
325
|
import { createRefreshInstance, createRefreshInstanceNow, type RefreshInstanceParams } from "./refresh";
|
|
326
|
+
import {
|
|
327
|
+
ControlResetError,
|
|
328
|
+
RuntimeReplacedError,
|
|
329
|
+
controlResetErr,
|
|
330
|
+
execFailureDocument,
|
|
331
|
+
runtimeReplacedErr,
|
|
332
|
+
selfAndCauses,
|
|
333
|
+
threadErrBuilders,
|
|
334
|
+
type ThreadErr,
|
|
335
|
+
} from "./threadErr";
|
|
326
336
|
import {
|
|
327
337
|
DEFAULT_EXEC_TIMEOUT_MS,
|
|
328
338
|
DEPS_STEP_OVERHEAD_MS,
|
|
@@ -621,9 +631,11 @@ const ATTACH_MUTEX_WAIT_MS = 60_000;
|
|
|
621
631
|
* stays far inside the bot's 45-min run budget. Work beyond 20 minutes
|
|
622
632
|
* belongs in background jobs.
|
|
623
633
|
*
|
|
624
|
-
* /op runs (test/build)
|
|
625
|
-
* path has no caller-supplied knob,
|
|
626
|
-
|
|
634
|
+
* /op runs (test/build) get the flat 20-minute ceiling — the deterministic op
|
|
635
|
+
* path has no caller-supplied knob, and a repo's full suite routinely outlives
|
|
636
|
+
* the 5-minute per-command default; the client bounds its /op wait at the same
|
|
637
|
+
* ceiling, so the two ends agree. */
|
|
638
|
+
const OP_EXEC_TIMEOUT_MS = BASH_TIMEOUT_MAX_MS;
|
|
627
639
|
/** Sanity bound on /exec's command body — a guard against a runaway caller,
|
|
628
640
|
* not a working limit: legitimate agent one-liners (heredocs writing test
|
|
629
641
|
* files, `node -e` scripts, long pipelines) run well past the old 8 000 and
|
|
@@ -674,70 +686,10 @@ export function validateEnvNames(vars: Record<string, string>): void {
|
|
|
674
686
|
}
|
|
675
687
|
}
|
|
676
688
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
* (the start RPC itself; the SDK never proves the process did NOT start) or
|
|
682
|
-
* `"collect"` (a `StaleProcessHandleError` on an already-running process). In
|
|
683
|
-
* both cases the command may have run, so the resident never re-issues it;
|
|
684
|
-
* the thread routes answer the NAMED `runtime-replaced` error and the client
|
|
685
|
-
* decides (idempotent read/write retry; exec is handed to the model). */
|
|
686
|
-
class RuntimeReplacedError extends Error {
|
|
687
|
-
constructor(
|
|
688
|
-
/** Where the replacement was met: the command's spawn, its collect, or
|
|
689
|
-
* the Worker's call into the Durable Object itself (`threadRejectionErr`),
|
|
690
|
-
* before the method could answer. */
|
|
691
|
-
readonly phase: "spawn" | "collect" | "call",
|
|
692
|
-
readonly cause: unknown,
|
|
693
|
-
/** Whether the replacement was KNOWN where the failure was classified
|
|
694
|
-
* (`replacementKnown`): the SDK vouched the runtime moved, or a restore is
|
|
695
|
-
* under way for this resident. One decision, made once at the exec choke
|
|
696
|
-
* point, and it gates the word on `/exec` alone (`replacedExecAnswer`) —
|
|
697
|
-
* the incarnation swap is unconditional there. Unknown, the failure only
|
|
698
|
-
* says the container is down or the transport was lost — a merely asleep
|
|
699
|
-
* or starting container, or a network blip, say the same — so the answer
|
|
700
|
-
* is the SDK's words and the harness's one more command decides. At the
|
|
701
|
-
* Worker's call nothing vouched, so it is `false` there. */
|
|
702
|
-
readonly known: boolean,
|
|
703
|
-
) {
|
|
704
|
-
super(
|
|
705
|
-
`runtime-replaced: the resident runtime was replaced (a deploy) while this command was ${
|
|
706
|
-
phase === "spawn" ? "starting" : phase === "collect" ? "running" : "pending at the Worker"
|
|
707
|
-
}; its output is lost (${errMsg(cause)})`,
|
|
708
|
-
);
|
|
709
|
-
this.name = "RuntimeReplacedError";
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/** The resident's own Durable Object was reset while a command was in flight —
|
|
714
|
-
* a `wrangler deploy` of the Worker code (not the container image) supersedes
|
|
715
|
-
* this DO's isolate, so the SDK's control session to the container is lost
|
|
716
|
-
* mid-command (`isDurableObjectCodeUpdateReset`). This is NOT a runtime
|
|
717
|
-
* replacement: the container and every process the run holds in it are exactly
|
|
718
|
-
* as they were — only the DO that was driving them reset. The command's
|
|
719
|
-
* outcome is unknown (the reset may have raced its start or its finish), so
|
|
720
|
-
* the resident never re-issues it here; the thread routes answer the NAMED
|
|
721
|
-
* `control-reset` error and the client re-sends an idempotent op or resolves a
|
|
722
|
-
* write by pi's echo (docs/reference/specs/harness-pi.md item 16). Distinct
|
|
723
|
-
* from `RuntimeReplacedError` precisely so the harness never mistakes a DO
|
|
724
|
-
* reset over a live pi for a replaced container and orphans that pi. */
|
|
725
|
-
class ControlResetError extends Error {
|
|
726
|
-
constructor(
|
|
727
|
-
/** Where the reset was met: the command's spawn, its collect, or the
|
|
728
|
-
* Worker's call into the Durable Object itself (`threadRejectionErr`),
|
|
729
|
-
* before the method could answer. */
|
|
730
|
-
readonly phase: "spawn" | "collect" | "call",
|
|
731
|
-
readonly cause: unknown,
|
|
732
|
-
) {
|
|
733
|
-
super(
|
|
734
|
-
`control-reset: the resident's Durable Object was reset (a deploy) while this command was ${
|
|
735
|
-
phase === "spawn" ? "starting" : phase === "collect" ? "running" : "pending at the Worker"
|
|
736
|
-
}; the container and its processes are as they were; the command's outcome is unknown (${errMsg(cause)})`,
|
|
737
|
-
);
|
|
738
|
-
this.name = "ControlResetError";
|
|
739
|
-
}
|
|
740
|
-
}
|
|
689
|
+
// `RuntimeReplacedError`, `ControlResetError`, the `ThreadErr` shape and the
|
|
690
|
+
// builders that answer a throw no route named live in ./threadErr.ts, pure
|
|
691
|
+
// over `(err, route)` so plain Node runs them; this entry hands them the
|
|
692
|
+
// predicates only it can supply (`threadErrBuilders`, below the predicates).
|
|
741
693
|
|
|
742
694
|
/** Every wording the pinned SDK (@cloudflare/sandbox@0.13.0-next.751.1) uses
|
|
743
695
|
* when the runtime incarnation changed under a call, for the message-based
|
|
@@ -755,17 +707,6 @@ class ControlResetError extends Error {
|
|
|
755
707
|
// port-ready bound. Both take the message fallback below; neither has a typed
|
|
756
708
|
// class to match.
|
|
757
709
|
|
|
758
|
-
/** `err` and its `cause` chain, bounded like the SDK's own `selfAndCauses`
|
|
759
|
-
* walker: the SDK wraps platform errors, so the telling message can sit one or
|
|
760
|
-
* two links down. */
|
|
761
|
-
function* selfAndCauses(err: unknown): Generator<unknown> {
|
|
762
|
-
let current = err;
|
|
763
|
-
for (let depth = 0; depth < 8 && current != null; depth++) {
|
|
764
|
-
yield current;
|
|
765
|
-
current = typeof current === "object" ? (current as { cause?: unknown }).cause : undefined;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
710
|
/** `OperationInterruptedError` reasons that mean the runtime under the call is
|
|
770
711
|
* gone and a new one serves the thread: a deploy swapped the isolate
|
|
771
712
|
* (`runtime_replaced`), the sandbox's lifetime epoch moved
|
|
@@ -848,107 +789,17 @@ function sdkVouchesRuntimeMoved(err: unknown): boolean {
|
|
|
848
789
|
return false;
|
|
849
790
|
}
|
|
850
791
|
|
|
851
|
-
/** The
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* the
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
* `worker-unavailable` is a re-probe after the harness's bounded wait
|
|
863
|
-
* (`replacedVerdict`, `PROBE_WAIT_MAX_MS`), never a tight retry, and a full
|
|
864
|
-
* queue drains. The platform's sentences alone — a bare `internal error` or
|
|
865
|
-
* `overloaded` is a word a git or GitHub failure carries too, and a throw
|
|
866
|
-
* typed transient by it would be re-probed for a restore window that never
|
|
867
|
-
* clears it. */
|
|
868
|
-
const TRANSIENT_PLATFORM_WORDING = /storage operation|durable object is overloaded/i;
|
|
869
|
-
|
|
870
|
-
/** The platform's own transient that no route word names — what is left once
|
|
871
|
-
* `threadRejectionErr` has answered a reset and a replacement by their words:
|
|
872
|
-
* the container's runtime unreachable (`isRuntimeUnreachable`: the SDK's
|
|
873
|
-
* connect abort by its `AbortError` name or its exact sentence —
|
|
874
|
-
* `RUNTIME_UNREACHABLE_WORDING` is anchored to the whole message, so a
|
|
875
|
-
* `TimeoutError`'s `The operation was aborted due to timeout`, the text a
|
|
876
|
-
* route's own `AbortSignal.timeout` on a slow GitHub raises, never matches),
|
|
877
|
-
* the pinned SDK's own platform-transient predicate (`isPlatformTransientError`:
|
|
878
|
-
* the code-update reset, a lost connection, the storage-startup reset, the
|
|
879
|
-
* typed `retryable` flag — the SDK's signal, read as it reads it), and the
|
|
880
|
-
* remainder sentences above, anywhere in the cause chain. */
|
|
881
|
-
function isUnnamedPlatformTransient(err: unknown): boolean {
|
|
882
|
-
if (isRuntimeUnreachable(err) || isPlatformTransientError(err)) return true;
|
|
883
|
-
for (const link of selfAndCauses(err)) {
|
|
884
|
-
if (link instanceof Error && TRANSIENT_PLATFORM_WORDING.test(link.message)) return true;
|
|
885
|
-
}
|
|
886
|
-
return false;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/** Whether a throw no route named is the platform's own transient — the
|
|
890
|
-
* Durable Object reset by a deploy (`isControlReset`), the runtime replaced
|
|
891
|
-
* under the call (`isRuntimeReplacement`), or the transient no word names
|
|
892
|
-
* (`isUnnamedPlatformTransient`) — against a deterministic throw in the route
|
|
893
|
-
* itself (a bug, a bad argument). The client reads the answer's `transient`
|
|
894
|
-
* field to re-probe the first and judge the second at once (execution.md item
|
|
895
|
-
* 9), never the words. */
|
|
896
|
-
function isTransientPlatformThrow(err: unknown): boolean {
|
|
897
|
-
return isControlReset(err) || isRuntimeReplacement(err) || isUnnamedPlatformTransient(err);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/** The 500 for a throw no route named, at whichever catch met it — the fetch
|
|
901
|
-
* handler's catch-all, a streamed route's rejection mapper, a route's own
|
|
902
|
-
* catch around its body (`prefix`: `attach-failed`, `op-failed`): the words,
|
|
903
|
-
* and whether the throw was the platform's transient (`transient`), typed
|
|
904
|
-
* here so the client decides by a field and never by which catch met the
|
|
905
|
-
* throw. A failure a route DID name — a step that failed, a mirror held — is
|
|
906
|
-
* that route's own answer and never comes here. */
|
|
907
|
-
function catchAllErr(err: unknown, prefix?: string): ThreadErr {
|
|
908
|
-
return unnamedThrowErr(err, isTransientPlatformThrow(err), prefix);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
/** The one shape of that 500: the words, behind `prefix` where the route names
|
|
912
|
-
* itself, and the verdict `transient`, decided once by the caller — so a
|
|
913
|
-
* caller that has already settled the typed predicates (`threadRejectionErr`)
|
|
914
|
-
* hands its verdict in instead of walking the cause chain again. */
|
|
915
|
-
function unnamedThrowErr(err: unknown, transient: boolean, prefix?: string): ThreadErr {
|
|
916
|
-
const words = errMsg(err);
|
|
917
|
-
return { error: prefix ? `${prefix}: ${words}` : words, status: 500, transient };
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
type ThreadDataRoute = "/exec" | "/read" | "/write";
|
|
921
|
-
|
|
922
|
-
/** A thread data-plane route's pending Durable Object call that REJECTED —
|
|
923
|
-
* the stub, not the method: the DO reset by a code update before or while the
|
|
924
|
-
* method ran, a storage operation that did not complete, the runtime
|
|
925
|
-
* unreachable — answered as the DO answers the same fact when it catches it
|
|
926
|
-
* inside (`execThreadImpl`, the file methods). A control reset is the DO's
|
|
927
|
-
* own word, `control-reset` on a 409: the container and its processes are as
|
|
928
|
-
* they were and the command's outcome is unknown, so the client resolves it by
|
|
929
|
-
* its own rule and never waits on it as the resident unavailable. A runtime
|
|
930
|
-
* replacement is answered by route, as the methods answer it, with the DO's
|
|
931
|
-
* own gate (`replacementKnown`) applied as far as it reaches: its first half,
|
|
932
|
-
* the SDK vouching the runtime moved (`sdkVouchesRuntimeMoved`), judges the
|
|
933
|
-
* moved sentences that survive the stub boundary as text, so it is made here
|
|
934
|
-
* by the same rule; its other half, a restore under way (`knowsContainerGone`),
|
|
935
|
-
* is the DO's alone and out of reach. So on `/exec` the word is said where
|
|
936
|
-
* the SDK vouched and WITHHELD where only the DO could have — that gate's
|
|
937
|
-
* unknown branch, the SDK's words on a 409 with no word, for the harness's one
|
|
938
|
-
* more command to judge; on `/read` and `/write` the word is said either way
|
|
939
|
-
* (`runtimeReplacedErr`, unconditional there on purpose), since it drives the
|
|
940
|
-
* client's one re-attach-and-retry and never a verdict. Anything else is the
|
|
941
|
-
* typed 500, its `transient` decided here once the typed predicates are
|
|
942
|
-
* settled. */
|
|
943
|
-
function threadRejectionErr(err: unknown, route: ThreadDataRoute): ThreadErr {
|
|
944
|
-
if (isControlReset(err)) return controlResetErr(new ControlResetError("call", err));
|
|
945
|
-
if (isRuntimeReplacement(err)) {
|
|
946
|
-
const known = sdkVouchesRuntimeMoved(err);
|
|
947
|
-
if (route === "/exec" && !known) return { error: errMsg(err), status: 409 };
|
|
948
|
-
return runtimeReplacedErr(new RuntimeReplacedError("call", err, known));
|
|
949
|
-
}
|
|
950
|
-
return unnamedThrowErr(err, isUnnamedPlatformTransient(err));
|
|
951
|
-
}
|
|
792
|
+
/** The 500 for a throw no route named and the thread data plane's rejection
|
|
793
|
+
* mapping (threadErr.ts holds the rules; threadErr.test.ts runs them under
|
|
794
|
+
* plain Node with the SDK's real sentences), built over the predicates only
|
|
795
|
+
* this entry can supply: the SDK's reset and platform-transient predicates,
|
|
796
|
+
* and the resident's own replacement classifier and vouch. */
|
|
797
|
+
const { catchAllErr, threadRejectionErr } = threadErrBuilders({
|
|
798
|
+
isControlReset,
|
|
799
|
+
isRuntimeReplacement,
|
|
800
|
+
sdkVouchesRuntimeMoved,
|
|
801
|
+
isPlatformTransientError,
|
|
802
|
+
});
|
|
952
803
|
|
|
953
804
|
/** Whether a command's failure is the resident's own Durable Object resetting
|
|
954
805
|
* under it (a Worker-code deploy) rather than the container runtime being
|
|
@@ -960,31 +811,6 @@ function isControlReset(err: unknown): boolean {
|
|
|
960
811
|
return isDurableObjectCodeUpdateReset(err);
|
|
961
812
|
}
|
|
962
813
|
|
|
963
|
-
/** The named ThreadErr every thread route (exec/read/write) answers for a
|
|
964
|
-
* runtime replacement, so the client can classify it (409 like the other
|
|
965
|
-
* recoverable thread states; `reason` is the discriminator). On `/exec` the
|
|
966
|
-
* answer goes through `replacedExecAnswer` first: the word only when the
|
|
967
|
-
* replacement was known where `run()` classified the failure. On
|
|
968
|
-
* `/read` and `/write` the word stays unconditional on purpose: those routes
|
|
969
|
-
* reach the same `run()` and can meet the same down wordings, but there the
|
|
970
|
-
* word drives the client's re-attach-and-retry (the attach is what wakes a
|
|
971
|
-
* container that is asleep or starting), never a verdict — nothing relaunches
|
|
972
|
-
* on it, and the harness never reads or writes through them (its seam runs
|
|
973
|
-
* every operation as an `/exec` script). Gating them would trade a spare
|
|
974
|
-
* re-attach for a read that fails outright while the container starts. */
|
|
975
|
-
function runtimeReplacedErr(err: RuntimeReplacedError): ThreadErr {
|
|
976
|
-
return { error: err.message, status: 409, reason: "runtime-replaced" };
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/** The named ThreadErr a DO code-update reset answers with — its own `reason`
|
|
980
|
-
* the client keys on, distinct from `runtime-replaced`: the container is
|
|
981
|
-
* unchanged and the command's outcome is unknown, so the client re-sends an
|
|
982
|
-
* idempotent op or resolves a write by echo (harness-pi item 16), never the
|
|
983
|
-
* replaced verdict. */
|
|
984
|
-
function controlResetErr(err: ControlResetError): ThreadErr {
|
|
985
|
-
return { error: err.message, status: 409, reason: "control-reset" };
|
|
986
|
-
}
|
|
987
|
-
|
|
988
814
|
/** The container's control port never answered: `exec` rejected with the
|
|
989
815
|
* DOMException of the SDK's connect abort (`DEFAULT_CONNECT_TIMEOUT_MS`,
|
|
990
816
|
* 30 s), raised inside its wake path — `RuntimeBootstrapProbe.probe` →
|
|
@@ -1308,36 +1134,6 @@ interface ThreadBinding {
|
|
|
1308
1134
|
depsKey?: string;
|
|
1309
1135
|
}
|
|
1310
1136
|
|
|
1311
|
-
/** Named, RPC-cloneable error shape for the thread data plane. The Worker
|
|
1312
|
-
* maps `status` to the HTTP status; extra fields (`needs`, `state`,
|
|
1313
|
-
* `reason`) ride along into the body. */
|
|
1314
|
-
interface ThreadErr {
|
|
1315
|
-
error: string;
|
|
1316
|
-
status: number;
|
|
1317
|
-
/** Beside `state` on a 503 that carries the lifecycle state: the lifecycle
|
|
1318
|
-
* REASON (`getStatus().reason`), kept apart from `reason`, which is the
|
|
1319
|
-
* answer's own word (`mirror-busy`, `disk-pressure`, `image-stale`). The
|
|
1320
|
-
* client reads the pair to decide whether the resident is coming back
|
|
1321
|
-
* (execution.md item 9); reading `reason` there would take a busy mirror on
|
|
1322
|
-
* a degraded-but-serviceable resident for a repo failure. */
|
|
1323
|
-
stateReason?: string;
|
|
1324
|
-
/** On the 500 for a throw no route named (`unnamedThrowErr`, through
|
|
1325
|
-
* `catchAllErr` or `threadRejectionErr`: the fetch handler's catch-all, a
|
|
1326
|
-
* streamed route's rejection, a route's own catch around its body): whether
|
|
1327
|
-
* the throw it wrapped was the platform's own transient
|
|
1328
|
-
* (`isTransientPlatformThrow`) — a re-probe clears it — or a deterministic
|
|
1329
|
-
* throw in the route, judged at once. */
|
|
1330
|
-
transient?: boolean;
|
|
1331
|
-
/** The steps the request ran before it failed (docs/reference/specs/tracing.md item 19):
|
|
1332
|
-
* a failed attach's trace is the one that says which step blew the budget. */
|
|
1333
|
-
trace?: ResidentStep[];
|
|
1334
|
-
needs?: string;
|
|
1335
|
-
/** With needs:"ref" — the resident's default branch, so the caller can bind by default. */
|
|
1336
|
-
defaultRef?: string;
|
|
1337
|
-
state?: ResidentState;
|
|
1338
|
-
reason?: string;
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
1137
|
interface AttachOk {
|
|
1342
1138
|
workspace: string;
|
|
1343
1139
|
ref: string;
|
|
@@ -8990,33 +8786,6 @@ function streamHeartbeatJson<T>(
|
|
|
8990
8786
|
return new Response(stream, { headers: { "content-type": "application/json" } });
|
|
8991
8787
|
}
|
|
8992
8788
|
|
|
8993
|
-
/** /exec's failure document, in the item-3 dual shape (`error` beside
|
|
8994
|
-
* `stdout: ""`, `stderr`, `exitCode: 127`) so old and new executors both
|
|
8995
|
-
* render it. The ThreadErr's fields ride beside the words, as the JSON routes
|
|
8996
|
-
* carry them: `needs`; the lifecycle pair (`state`, `stateReason`); the
|
|
8997
|
-
* answer's own word (`reason`, kept independent of `state` — `runtimeReplacedErr()`
|
|
8998
|
-
* sets `reason: "runtime-replaced"` with NO state, and the client's
|
|
8999
|
-
* deploy-vs-dead-transport check reads it); the answer's `status`; the
|
|
9000
|
-
* catch-all's `transient`. The client types a streamed failure by these fields
|
|
9001
|
-
* (execution.md item 9), and a document that dropped them would make every
|
|
9002
|
-
* /exec failure a deterministic answer over HTTP 200. One builder for both of
|
|
9003
|
-
* the stream's paths: a failure the Durable Object named, and a pending
|
|
9004
|
-
* result that rejected. */
|
|
9005
|
-
function execFailureDocument(failure: ThreadErr): object {
|
|
9006
|
-
return {
|
|
9007
|
-
error: failure.error,
|
|
9008
|
-
...(failure.needs ? { needs: failure.needs } : {}),
|
|
9009
|
-
...(failure.state ? { state: failure.state } : {}),
|
|
9010
|
-
...(typeof failure.stateReason === "string" ? { stateReason: failure.stateReason } : {}),
|
|
9011
|
-
...(failure.reason ? { reason: failure.reason } : {}),
|
|
9012
|
-
status: failure.status,
|
|
9013
|
-
...(typeof failure.transient === "boolean" ? { transient: failure.transient } : {}),
|
|
9014
|
-
stdout: "",
|
|
9015
|
-
stderr: failure.error,
|
|
9016
|
-
exitCode: 127,
|
|
9017
|
-
};
|
|
9018
|
-
}
|
|
9019
|
-
|
|
9020
8789
|
/** /exec's payload mapping: a result as {stdout, stderr, exitCode, truncated};
|
|
9021
8790
|
* a failure as `execFailureDocument` — the one the Durable Object named, or
|
|
9022
8791
|
* the pending result's rejection answered as the DO would have
|
|
@@ -49,7 +49,11 @@ import {
|
|
|
49
49
|
import { envFromRequest } from "../../src/execution/sandboxEnv.js";
|
|
50
50
|
import { IdleGuard, type IdleGuardHost } from "../../src/execution/sandboxIdle.js";
|
|
51
51
|
import { StartGate, type StartGateHost, type StartingCause } from "../../src/execution/sandboxStart.js";
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
RUNTIME_REPLACEMENT_WORDING,
|
|
54
|
+
isRuntimeUnreachableSignal,
|
|
55
|
+
selfAndCauses,
|
|
56
|
+
} from "../../src/execution/residentRefresh.js";
|
|
53
57
|
import {
|
|
54
58
|
fleetBusyAnswer,
|
|
55
59
|
fleetBusyExecAnswer,
|
|
@@ -149,15 +153,6 @@ const RUNTIME_REPLACED_REASONS = new Set([
|
|
|
149
153
|
* closed (a runtime crash or stop), the socket failed, the upgrade failed. */
|
|
150
154
|
const RPC_TRANSPORT_LOSS_KINDS = new Set(["peer_closed", "connection_failed", "upgrade_failed", "session_disposed"]);
|
|
151
155
|
|
|
152
|
-
/** `err` and its `cause` chain, bounded like the SDK's own walk. */
|
|
153
|
-
function* selfAndCauses(err: unknown): Generator<unknown> {
|
|
154
|
-
let link: unknown = err;
|
|
155
|
-
for (let depth = 0; link !== null && link !== undefined && depth < 8; depth++) {
|
|
156
|
-
yield link;
|
|
157
|
-
link = typeof link === "object" ? (link as { cause?: unknown }).cause : undefined;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
156
|
/** Did the container's runtime change under the command? Typed first (the
|
|
162
157
|
* Durable Object sees the SDK's own classes, so `instanceof` holds here), the
|
|
163
158
|
* SDK's replacement wording second — the same list the resident Worker
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
"workers": ["bot"],
|
|
17
17
|
"note": "Anthropic Console key the bot runs on. Third-party."
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
"name": "OPENAI_API_KEY",
|
|
21
|
+
"workers": ["bot"],
|
|
22
|
+
"optional": true,
|
|
23
|
+
"note": "OpenAI key for the example config's `openai` provider block (`apiKeyEnv: OPENAI_API_KEY`). Third-party. Optional: read at the first model call on that block, never at load; without it every other block runs and only a run whose model names `openai/…` fails, by this name."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "OPENROUTER_API_KEY",
|
|
27
|
+
"workers": ["bot"],
|
|
28
|
+
"optional": true,
|
|
29
|
+
"note": "OpenRouter key for the example config's `openrouter` provider block (`apiKeyEnv: OPENROUTER_API_KEY`). Third-party. Optional: read at the first model call on that block, never at load; without it every other block runs and only a run whose model names `openrouter/…` fails, by this name."
|
|
30
|
+
},
|
|
19
31
|
{
|
|
20
32
|
"name": "ANTHROPIC_ADMIN_KEY",
|
|
21
33
|
"workers": ["bot"],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.245.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.245.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.245.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.245.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
|
@@ -145,7 +145,7 @@ export function statusCardRule(examples = '"Implement the fix", "Run the test su
|
|
|
145
145
|
|
|
146
146
|
const PR_DESCRIPTION_TEMPLATE = `PR description — submit it with the submit_pr_description tool for EVERY PR (this is the default, not something to wait to be asked for). Switchboard renders the GitHub body from the object you submit, so never author PR-body markdown yourself. Before submitting, judge your title with the ${PR_TITLE_GUARD} gate — \`npm run check:pr-title -- "<title>"\` — and submit only a title it accepts; the same gate refuses the PR in CI. The body is a fixed-size MAP for the reader with everything for agents collapsed under it; every field is capped in visible characters (a link's URL is not counted) and the tool refuses an object over a cap naming the field and the count — cut and resubmit. Prose is unwrapped — no hard line breaks inside a paragraph. Always hyperlink the triggering issue/request. Never fabricate validation — state exactly what you ran and the real result. BEFORE authoring the pointers, load the \`pr-description\` skill with use_skill — it defines how to choose at most seven pointers, the mechanical anchor rules and what goes below the fold; follow it for every PR.
|
|
147
147
|
EVERY PR includes one that already exists when you push — opened by a person, by dependabot, or by an earlier run. After EVERY push to such a PR: read its current title and body (\`github_issue_get\` with the PR number works for pull requests; \`gh pr view\` where gh exists), judge them against the change as it now stands at the pushed head, and submit the object that describes the PR as it is NOW — carry forward what the existing body says that is still true (a dependency bump's release notes belong in why), add what you changed, and anchor the pointers at the new head. Switchboard replaces the PR's title and body with your rendering. A description that describes an earlier state of its branch is a bug; "it is someone else's PR" is never a reason to leave it.
|
|
148
|
-
- **title
|
|
148
|
+
- **title** (≤72 characters in all): the changelog line — \`type(scope): what a reader can now do or expect\`, one change, one clause, present tense. The type and the scope spend the same budget as the description, so the description is short; the body carries the rest. The tool refuses a longer title naming the count, and so does the gate in CI.
|
|
149
149
|
- **TL;DR** (\`tldr\`, rendered first, ≤300): two sentences for a reader with zero context — what this PR does and why it matters.
|
|
150
150
|
- **Why** (\`why\`, ≤400): the problem and the motivation, with the triggering issue/request, the record and the stack position hyperlinked. Why, never what: the diff shows what.
|
|
151
151
|
- **Where to look** (\`pointers\`, 1 to 7): the files a reviewer would open first, in reading order, each { label ≤60, text ≤160, optional risk ≤100, anchor } with the anchor a { path, from, to } line range at your pushed head, rendered as a link (never embedded code). One pointer per idea, never per file; when the change has more ideas than seven, keep the seven whose mistake would cost most.
|
|
@@ -357,9 +357,9 @@ Your final message is posted to Slack — keep it readable, lead with the outcom
|
|
|
357
357
|
|
|
358
358
|
// Both review prompts carry this verbatim. The findings contract
|
|
359
359
|
// (docs/reference/specs/agent-ship.md item 6) lives here once — stable ids, the severity
|
|
360
|
-
// vocabulary, the
|
|
361
|
-
// resident variants can never drift apart on it.
|
|
362
|
-
const REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no
|
|
360
|
+
// vocabulary, the severity gate's downgrade (agent-review.md item 5a) — so the
|
|
361
|
+
// sandbox and resident variants can never drift apart on it.
|
|
362
|
+
const REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no finding at or above the severity to address remains — the level in force for this run, \`minor\` by default: a major or a minor finding means \`request_changes\`; nits alone never block) or \`request_changes\`, a one-line summary, \`head\` = the output of \`git rev-parse HEAD\` in the checkout you reviewed, and \`findings\` — every issue you report as a structured entry with a stable id you assign in order (F1, F2, …), a severity of exactly blocking|major|minor|nit, the file (plus line when it points at one), and a one-line title. The findings array is the index of your review: the full explanation of each finding stays in your prose, keyed by the same ids. Switchboard writes the verdict as the first line of the GitHub comment itself and lists the findings under it; a review with no submitted verdict is posted as not approving, so never skip it. An \`approve\` carrying a finding at or above the severity to address is downgraded to \`request_changes\` and the tool's ack says so — approve only when every finding sits below the level. Do not write "LGTM" in your own text — the verdict line carries it.`;
|
|
363
363
|
|
|
364
364
|
// The diff-gated spec review (docs/reference/specs/agent-review.md item 14) and
|
|
365
365
|
// the test guard under it (item 16; specs-coverage.md item 6), one text for
|
|
@@ -231,7 +231,11 @@ export function boundedByParent(
|
|
|
231
231
|
* reply names. */
|
|
232
232
|
export type ProfileRefusal =
|
|
233
233
|
| { axis: "identity"; needs: Identity; cap: Identity; scope: BoundaryScope }
|
|
234
|
-
| { axis: "machine"; needs: MachineClass; allowed: MachineClass[]; scopes: BoundaryScope[] }
|
|
234
|
+
| { axis: "machine"; needs: MachineClass; allowed: MachineClass[]; scopes: BoundaryScope[] }
|
|
235
|
+
/** The minutes axis refuses only under a minimum the caller names (the
|
|
236
|
+
* preset's lease minimum, `leaseMinimum` in `src/core/budgets.ts`): a lease
|
|
237
|
+
* clipped below it would leave the loop no time at all. */
|
|
238
|
+
| { axis: "minutes"; needs: number; have: number; scope: BoundaryScope };
|
|
235
239
|
|
|
236
240
|
/** The effective profile, or the refusal the authorize stage turns into a
|
|
237
241
|
* named reply. Resolution is pure; the gate judges. */
|
|
@@ -252,6 +256,8 @@ export function effectiveProfile(
|
|
|
252
256
|
preset: Pick<AgentDef, "machine" | "identity" | "maxMinutes">,
|
|
253
257
|
directives: { budget?: number },
|
|
254
258
|
boundary: EffectiveBoundary | undefined,
|
|
259
|
+
/** The least lease the preset does anything under; absent, the minutes only clip. */
|
|
260
|
+
minimum?: number,
|
|
255
261
|
): ProfileResolution {
|
|
256
262
|
if (boundary?.maxIdentity && !identityWithin(preset.identity, boundary.maxIdentity.value)) {
|
|
257
263
|
return {
|
|
@@ -285,6 +291,16 @@ export function effectiveProfile(
|
|
|
285
291
|
minutes = boundary.maxMinutes.value;
|
|
286
292
|
boundedBy = boundary.maxMinutes.scope;
|
|
287
293
|
}
|
|
294
|
+
// Under the minimum the clip is a refusal, not a run: the write-up and the
|
|
295
|
+
// post-step take the whole lease and the loop never turns. Attributed to
|
|
296
|
+
// whatever clipped; a declared ask under its own minimum is the module's
|
|
297
|
+
// invariant to catch, named as the defaults' here so the switch is total.
|
|
298
|
+
if (minimum !== undefined && minutes < minimum) {
|
|
299
|
+
return {
|
|
300
|
+
kind: "refused",
|
|
301
|
+
refusal: { axis: "minutes", needs: minimum, have: minutes, scope: boundedBy ?? "defaults" },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
288
304
|
return {
|
|
289
305
|
kind: "profile",
|
|
290
306
|
profile: {
|
|
@@ -66,6 +66,14 @@ export interface Actor {
|
|
|
66
66
|
* → nothing: fail-closed, exactly today's behaviour.
|
|
67
67
|
*/
|
|
68
68
|
readonly memberOf?: ReadonlySet<string>;
|
|
69
|
+
/**
|
|
70
|
+
* The person an admin's dashboard session is viewing as (record 0053): set
|
|
71
|
+
* beside `onBehalfOf` = that person's actor, so the effective grants are the
|
|
72
|
+
* intersection (the person's) and `self`/`memberOf` are the person's, while
|
|
73
|
+
* `id` stays the admin's. Identity for the audit line and the banner; the
|
|
74
|
+
* registry door refuses every non-read effect while it is set. No rule reads it.
|
|
75
|
+
*/
|
|
76
|
+
readonly viewingAs?: { readonly id: string; readonly name?: string };
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
/** `<group>:<read|write|exec>` plus the non-command actions. A plain
|
|
@@ -30,6 +30,19 @@ export const DAY_MS = 24 * 60 * MINUTE_MS;
|
|
|
30
30
|
* which stamps the expiry on its own clock. */
|
|
31
31
|
export const CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
|
|
32
32
|
|
|
33
|
+
/** How long a dispatch's FIRST attach to a resident — a fresh run's, or a
|
|
34
|
+
* resumed run's re-attach to its recorded worktree — waits for the resident
|
|
35
|
+
* to wake when the Worker typed its refusal as the platform's transient
|
|
36
|
+
* (docs/reference/specs/execution.md item 9): a Durable Object reset or lost
|
|
37
|
+
* under the attach clears in seconds, so a minute is generous, and far under
|
|
38
|
+
* the wake ceiling a mid-run re-attach may take — the card is silent while
|
|
39
|
+
* this wait runs, then names it. It bounds the PROBING: past it no further
|
|
40
|
+
* probe is made and, with nothing bound, a fresh run falls cold and a resumed
|
|
41
|
+
* run is refused, the wait named either way; a re-attach already opened
|
|
42
|
+
* inside it runs to the attach's own timeout. The run's own stop ends any of
|
|
43
|
+
* it at once. Read by the executor factory. */
|
|
44
|
+
export const FIRST_ATTACH_WAIT_MS = MINUTE_MS;
|
|
45
|
+
|
|
33
46
|
/** The presets that run the tool loop, and the one pipeline preset. */
|
|
34
47
|
export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
|
|
35
48
|
export type LoopPreset = (typeof LOOP_PRESETS)[number];
|
|
@@ -127,6 +140,18 @@ export function postStepLease(preset: string, remainingMs: number | undefined):
|
|
|
127
140
|
return Math.min(allowance, Math.max(1, remainingMs / MINUTE_MS));
|
|
128
141
|
}
|
|
129
142
|
|
|
143
|
+
/** The least lease under which a loop-running preset does anything: the
|
|
144
|
+
* write-up and the preset's post-step are held back from every lease
|
|
145
|
+
* (`loopClock`), so a lease of that sum leaves the loop nothing — the run
|
|
146
|
+
* goes straight to its write-up and reports a budget it never had. One minute
|
|
147
|
+
* above the sum is the least that runs a turn. A profile clipped under this by
|
|
148
|
+
* a directive, a boundary or a parent's remainder is refused at the gate
|
|
149
|
+
* naming it, the way a ship round is refused under its floor; the floors are
|
|
150
|
+
* the larger numbers a round needs to be worth dispatching. */
|
|
151
|
+
export function leaseMinimum(preset: string): number {
|
|
152
|
+
return ALLOWANCES.writeUp + postStepMinutes(preset) + 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
/** A follow-up turn's lease, in ms: the lesser of what it asks and what the
|
|
131
156
|
* run's lease still holds, never under a minute — the grace covers that floor
|
|
132
157
|
* when the turn starts by the lease's end; later, the bearer may expire under
|