@coreplane/switchboard 1.254.1 → 1.256.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/.dockerignore +3 -0
- package/dist/assets/Dockerfile +12 -1
- package/dist/assets/config/config.example.yaml +6 -1
- package/dist/assets/deploy/cloudflare/worker.ts +39 -23
- package/dist/assets/deploy/cloudflare-memory/worker.ts +552 -5
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +5 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +13 -1
- package/dist/assets/deploy/cloudflare-resident/drain.ts +55 -1
- package/dist/assets/deploy/cloudflare-resident/levels.ts +84 -0
- package/dist/assets/deploy/cloudflare-resident/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +358 -42
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -1
- package/dist/assets/deploy/cloudflare-sandbox/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +21 -4
- package/dist/assets/deploy/hooks/prepare-commit-msg +17 -0
- 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 +52 -0
- package/dist/assets/src/core/budgets.ts +35 -2
- package/dist/assets/src/core/coordinator/contract.ts +6 -0
- package/dist/assets/src/core/coordinator/driver.ts +59 -6
- package/dist/assets/src/core/costs.ts +39 -16
- package/dist/assets/src/core/pipelineStanding.ts +62 -0
- package/dist/assets/src/core/plane/decide.ts +389 -22
- package/dist/assets/src/core/refusal.ts +3 -0
- package/dist/assets/src/core/reviewVerdict.ts +64 -2
- package/dist/assets/src/core/runEvents.ts +31 -12
- package/dist/assets/src/core/runLedger/sessionLog.ts +128 -0
- package/dist/assets/src/core/runLedger/types.ts +3 -0
- package/dist/assets/src/core/runRecord.ts +24 -7
- package/dist/assets/src/core/ship/contract.ts +20 -30
- package/dist/assets/src/core/ship/coordinator.ts +519 -126
- package/dist/assets/src/core/trace/workerTrace.ts +3 -0
- package/dist/assets/src/execution/sandboxErrors.ts +77 -6
- package/dist/assets/web/dist/.vite/manifest.json +61 -60
- package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +1 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-ngPsO2to.js → DeliveryPage-BBAyLwPq.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-DvxTHzPx.js → HomePage-BEiStFwW.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-DGINv9XT.js +1 -0
- package/dist/assets/web/dist/assets/{PlanePage-DpWfiX4C.js → PlanePage-JEj-lqgz.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DG86v39Y.js → ResidentDetailPage-jYhEeeyu.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-x6p689VH.js → ResidentsIndexPage-DvFmlV4U.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DvWzR1JQ.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CvZ-TOT3.js +9 -0
- package/dist/assets/web/dist/assets/{RunsIndexPage-CuzFchcn.js → RunsIndexPage-JqVSDOok.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BuLmfcbG.js → ScheduledPage-Viim-bus.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BujWkdU_.js → SettingsPage-CYUy8McC.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-C8Bc0pTX.js → StatusDot-Dv6UMaPy.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BWwJx27K.js → Tooltip-Bd-5Rypv.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-B81wEnKN.js +1 -0
- package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +1 -0
- package/dist/assets/web/dist/assets/{dist-CpnyQGOb.js → dist-Nl3uaxrP.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-DborJPFp.js +1 -0
- package/dist/assets/web/dist/assets/{main-Comxmwi4.js → main-DRxWlffc.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-IzTdD4-3.js → sseReplay-DE6wv1Ua.js} +6 -6
- package/dist/cli.js +4400 -3079
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BuKjw3nv.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-ysJBY8xQ.js +0 -9
- package/dist/assets/web/dist/assets/UnitRoutePage-B9kjA1AT.js +0 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-DABQtONT.js +0 -1
|
@@ -58,6 +58,11 @@
|
|
|
58
58
|
// deploy/cloudflare/wrangler.template.jsonc). The class is deployed with the
|
|
59
59
|
// bot, which deploys after this Worker — so this binding lands one release
|
|
60
60
|
// after the class did, never in the same one.
|
|
61
|
+
// The bot Worker, for the plane's effect push (record 0064, "Where it
|
|
62
|
+
// lives"): committed effects POST to the bot's bearer-gated /plane/effects,
|
|
63
|
+
// which forwards them to the container. A push that fails is not retried by
|
|
64
|
+
// a timer — the effect rides the next heartbeat or reclaim-sweep answer.
|
|
65
|
+
"services": [{ "binding": "BOT", "service": "{{bot.script}}" }],
|
|
61
66
|
"workflows": [
|
|
62
67
|
{
|
|
63
68
|
"name": "{{bot.script}}-ship-coordinator",
|
|
@@ -22,8 +22,20 @@ FROM docker.io/cloudflare/sandbox:0.13.0-next.751.1
|
|
|
22
22
|
# installed here — matching the resident system prompts and the tests that pin
|
|
23
23
|
# that claim. No system credential.helper is configured; each worktree sets its
|
|
24
24
|
# own per-attach helper.
|
|
25
|
+
# The fallback user.email stays OFF the GitHub domain (a noreply-shaped
|
|
26
|
+
# address would render as a GitHub account it is not); the real pairs ride
|
|
27
|
+
# each /exec body's env. The system-level core.hooksPath replaces repo-local
|
|
28
|
+
# .git/hooks entirely — deliberate: an untrusted checkout's own hooks never
|
|
29
|
+
# run here (a repo-local core.hooksPath still wins, git's ordinary
|
|
30
|
+
# precedence) — docs/reference/specs/execution.md item 5.
|
|
25
31
|
RUN git config --system user.name "switchboard-resident" \
|
|
26
|
-
&& git config --system user.email "switchboard-resident@
|
|
32
|
+
&& git config --system user.email "switchboard-resident@switchboard.invalid" \
|
|
33
|
+
&& git config --system core.hooksPath /opt/switchboard/hooks
|
|
34
|
+
|
|
35
|
+
# The shared agent-trailer hook (deploy/hooks/prepare-commit-msg — this copy is
|
|
36
|
+
# byte-identical, held so by a test: the build context is this directory, so
|
|
37
|
+
# COPY cannot reach the canonical file).
|
|
38
|
+
COPY --chmod=0755 prepare-commit-msg /opt/switchboard/hooks/prepare-commit-msg
|
|
27
39
|
|
|
28
40
|
# The container is FOUR vCPUs shared by up to 16 threads (wrangler.jsonc).
|
|
29
41
|
# Node test runners size their worker pools from the CPU count the kernel
|
|
@@ -23,6 +23,17 @@ export interface DrainRecord {
|
|
|
23
23
|
by: string;
|
|
24
24
|
/** Why, in the words the attach refusal repeats. */
|
|
25
25
|
reason: string;
|
|
26
|
+
/** The residents whose containers still run the pre-deploy image (issue
|
|
27
|
+
* 1931): the deploy's reconcile marks the Durable Objects but the platform
|
|
28
|
+
* replaces the container processes asynchronously, so "reconciled" is not
|
|
29
|
+
* "swapped" — each hold stands until that resident reports its running
|
|
30
|
+
* container on the deploy's image, and the fleet reopens on the LAST
|
|
31
|
+
* report, a fact, never a wait. Absent or empty: no swap outstanding. */
|
|
32
|
+
holds?: string[];
|
|
33
|
+
/** Whether `POST /undrain` already asked for the lift while holds stood:
|
|
34
|
+
* the record then clears itself on the last hold's report instead of
|
|
35
|
+
* waiting for a second lift. */
|
|
36
|
+
liftAsked?: boolean;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
/** The longest a drain may run, and the default, from the one clock table
|
|
@@ -88,7 +99,50 @@ export function liveDrain(stored: unknown, now: number): DrainRecord | null {
|
|
|
88
99
|
return null;
|
|
89
100
|
const until = Date.parse(r.until);
|
|
90
101
|
if (!Number.isFinite(until) || until <= now) return null;
|
|
91
|
-
|
|
102
|
+
const holds = Array.isArray(r.holds) ? r.holds.filter((h): h is string => typeof h === "string") : [];
|
|
103
|
+
return {
|
|
104
|
+
since: r.since,
|
|
105
|
+
until: r.until,
|
|
106
|
+
by: r.by,
|
|
107
|
+
reason: r.reason,
|
|
108
|
+
...(holds.length > 0 ? { holds } : {}),
|
|
109
|
+
...(r.liftAsked === true ? { liftAsked: true } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The record with the named residents held (issue 1931): the deploy's
|
|
114
|
+
* reconcile could not verify their running containers on the new image, so
|
|
115
|
+
* the fleet must not reopen onto them until each reports. Deduplicated;
|
|
116
|
+
* an empty set changes nothing. */
|
|
117
|
+
export function holdDrain(record: DrainRecord, resources: readonly string[]): DrainRecord {
|
|
118
|
+
const holds = [...new Set([...(record.holds ?? []), ...resources])];
|
|
119
|
+
if (holds.length === 0) return record;
|
|
120
|
+
return { ...record, holds };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** `POST /undrain`'s decision over the stored record: with no holds the drain
|
|
124
|
+
* clears; with holds outstanding the fleet STAYS closed — the record keeps
|
|
125
|
+
* standing with `liftAsked`, so the last container's new-image report lifts
|
|
126
|
+
* it (a fact, never a timer; `until` remains the backstop for a report that
|
|
127
|
+
* never comes). */
|
|
128
|
+
export function liftDrain(record: DrainRecord): { cleared: true } | { cleared: false; record: DrainRecord } {
|
|
129
|
+
const holds = record.holds ?? [];
|
|
130
|
+
if (holds.length === 0) return { cleared: true };
|
|
131
|
+
return { cleared: false, record: { ...record, liftAsked: true } };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** One resident's word that its running container is on the deploy's image:
|
|
135
|
+
* its hold drops; when it was the last hold and the lift was already asked,
|
|
136
|
+
* the drain lifts here — the reopen fires on the last container's report. */
|
|
137
|
+
export function reportImageCurrent(
|
|
138
|
+
record: DrainRecord,
|
|
139
|
+
resource: string,
|
|
140
|
+
): { lifted: boolean; record: DrainRecord | null } {
|
|
141
|
+
const holds = (record.holds ?? []).filter((h) => h !== resource);
|
|
142
|
+
const next: DrainRecord = { ...record, ...(holds.length > 0 ? { holds } : {}) };
|
|
143
|
+
if (holds.length === 0) delete next.holds;
|
|
144
|
+
if (holds.length === 0 && record.liftAsked === true) return { lifted: true, record: null };
|
|
145
|
+
return { lifted: false, record: next };
|
|
92
146
|
}
|
|
93
147
|
|
|
94
148
|
/** The `/attach` answer while the fleet is drained: a 503 whose body carries
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The resident's level reports (record 0064; the orchestration plane's
|
|
2
|
+
// resident conditions) — the PURE half, kept free of the Sandbox SDK and DO
|
|
3
|
+
// storage so it runs under plain-Node vitest (levels.test.ts) like gc.ts,
|
|
4
|
+
// drain.ts and memoryGuard.ts. The Worker owns the readings (the user pool's
|
|
5
|
+
// allocation, the memory guard's last sample, its own incarnation) and feeds
|
|
6
|
+
// this module the numbers; every `/attach`, `/exec` and `/status` answer
|
|
7
|
+
// carries the document, and the bot forwards a change to the plane's
|
|
8
|
+
// `POST /plane/level`.
|
|
9
|
+
//
|
|
10
|
+
// Why an outbox and not a push: the resident never holds a credential for the
|
|
11
|
+
// state Worker, so it posts nothing itself — a crossing between calls is kept
|
|
12
|
+
// as a post and re-offered on every answer until a newer crossing of the same
|
|
13
|
+
// name supersedes it. The plane's level write is idempotent per (resident,
|
|
14
|
+
// name), so a post forwarded twice lands once.
|
|
15
|
+
|
|
16
|
+
import { MEMORY_SOFT_LIMIT_PCT } from "./memoryGuard.js";
|
|
17
|
+
|
|
18
|
+
/** Which side of its line a level reads: `above` closes the door (an exhausted
|
|
19
|
+
* pool, the gate's soft side), `below` opens it. */
|
|
20
|
+
export type LevelSide = "below" | "above";
|
|
21
|
+
|
|
22
|
+
/** One level post for the plane: the crossing (or a boot's re-statement) the
|
|
23
|
+
* bot forwards to `POST /plane/level`. */
|
|
24
|
+
export interface LevelPost {
|
|
25
|
+
/** `drain` is the registry's fleet-drain post; the residents post the other two. */
|
|
26
|
+
name: "seat" | "memory" | "drain";
|
|
27
|
+
side: LevelSide;
|
|
28
|
+
generation: string;
|
|
29
|
+
at: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The compact sides of one sample, persisted so the next sample can judge a
|
|
33
|
+
* crossing; `generation` names the incarnation the sample came from. */
|
|
34
|
+
export interface LevelSample {
|
|
35
|
+
seat: LevelSide;
|
|
36
|
+
memory: LevelSide;
|
|
37
|
+
generation: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The document every resident answer carries as `levels` (record 0064). */
|
|
41
|
+
export interface ResidentLevelsDoc {
|
|
42
|
+
seat: { side: LevelSide; used: number; total: number };
|
|
43
|
+
memory: { side: LevelSide; percent: number | null };
|
|
44
|
+
generation: string;
|
|
45
|
+
at: string;
|
|
46
|
+
/** The outbox: crossings not yet superseded, re-offered on every answer. */
|
|
47
|
+
posts: LevelPost[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The seat's side: the pool is a hard count, so `above` is exactly "no free
|
|
51
|
+
* user" — the next attach or op would be refused `user-pool-exhausted`. */
|
|
52
|
+
export function seatSide(used: number, total: number): LevelSide {
|
|
53
|
+
return used >= total ? "above" : "below";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The memory side is the gate's soft line (resident-repos item 70): at or
|
|
57
|
+
* past `MEMORY_SOFT_LIMIT_PCT` a NEW attach is refused, so that is where the
|
|
58
|
+
* plane's `memory` condition sits. No reading, or no cap, gates nothing. */
|
|
59
|
+
export function memorySide(percent: number | null): LevelSide {
|
|
60
|
+
return percent !== null && percent >= MEMORY_SOFT_LIMIT_PCT ? "above" : "below";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The posts one fresh sample owes the plane: each name whose side crossed
|
|
64
|
+
* since the previous sample — and, with no previous sample or one from
|
|
65
|
+
* another generation (a boot, a replaced runtime), both names re-stated
|
|
66
|
+
* (record 0064: "a boot re-states it"), whatever their sides. */
|
|
67
|
+
export function levelPosts(prev: LevelSample | null, next: LevelSample, at: string): LevelPost[] {
|
|
68
|
+
const restate = prev === null || prev.generation !== next.generation;
|
|
69
|
+
const posts: LevelPost[] = [];
|
|
70
|
+
if (restate || prev.seat !== next.seat)
|
|
71
|
+
posts.push({ name: "seat", side: next.seat, generation: next.generation, at });
|
|
72
|
+
if (restate || prev.memory !== next.memory)
|
|
73
|
+
posts.push({ name: "memory", side: next.memory, generation: next.generation, at });
|
|
74
|
+
return posts;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Merge fresh posts into the outbox: a newer post of a name supersedes the
|
|
78
|
+
* older one — the plane only needs the current side, and the bot forwarding
|
|
79
|
+
* a superseded post would only be overwritten by the next. The outbox never
|
|
80
|
+
* grows past one post per name. */
|
|
81
|
+
export function mergeOutbox(outbox: LevelPost[], posts: LevelPost[]): LevelPost[] {
|
|
82
|
+
const superseded = new Set(posts.map((p) => p.name));
|
|
83
|
+
return [...outbox.filter((p) => !superseded.has(p.name)), ...posts];
|
|
84
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The agent trailer (docs/reference/specs/execution.md): every commit made in a
|
|
3
|
+
# Switchboard image carries `Co-Authored-By: <bot pair>`, so the agent's hand
|
|
4
|
+
# stays visible even when the author is the requester. The pair is read from
|
|
5
|
+
# GIT_COMMITTER_NAME/GIT_COMMITTER_EMAIL at commit time — the bot fills them
|
|
6
|
+
# per exec — so the image stays installation-agnostic; without them the
|
|
7
|
+
# image's own git identity stands (its fallback address is off the GitHub
|
|
8
|
+
# domain, so it never renders as a GitHub account). Idempotent: a message that
|
|
9
|
+
# already carries this exact trailer is left unchanged; a foreign
|
|
10
|
+
# Co-Authored-By does not stop it (the identity rewrite scrubs those).
|
|
11
|
+
set -e
|
|
12
|
+
msg="$1"
|
|
13
|
+
name="${GIT_COMMITTER_NAME:-$(git config user.name || true)}"
|
|
14
|
+
email="${GIT_COMMITTER_EMAIL:-$(git config user.email || true)}"
|
|
15
|
+
[ -n "$name" ] && [ -n "$email" ] || exit 0
|
|
16
|
+
git interpret-trailers --in-place --if-exists addIfDifferent \
|
|
17
|
+
--trailer "Co-Authored-By: $name <$email>" "$msg"
|