@coreplane/switchboard 1.212.0 → 1.213.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/README.md +5 -3
- package/dist/assets/config/config.example.yaml +44 -0
- package/dist/assets/deploy/cloudflare/worker.ts +6 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +24 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +353 -18
- package/dist/assets/deploy/cloudflare-resident/wrangler.template.jsonc +9 -1
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +18 -0
- package/dist/assets/deploy/secrets.manifest.json +18 -0
- package/dist/assets/package-lock.json +5 -3
- package/dist/assets/package.json +2 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +37 -0
- package/dist/assets/src/core/redact.ts +11 -1
- package/dist/assets/src/core/runEvents.ts +41 -1
- package/dist/assets/src/core/runFriction.ts +6 -4
- package/dist/assets/src/core/trace/workerTrace.ts +4 -0
- package/dist/assets/src/execution/residentRefresh.ts +151 -5
- package/dist/assets/web/dist/.vite/manifest.json +19 -19
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CM5nWw-Z.js → ResidentDetailPage-CvwjhjlG.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BKBnuvp3.js → ResidentsIndexPage-DoN6XuNx.js} +1 -1
- package/dist/assets/web/dist/assets/RunRoutePage-FzLNexjF.js +12 -0
- package/dist/assets/web/dist/assets/{RunsIndexPage-w2jJP5xu.js → RunsIndexPage-DhSUbF9k.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-D3-k9DPz.js → ScheduledPage-Bq0Yg4nT.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BmFHnV8m.js → StatusDot-D8Wwt1KC.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DoThP2fW.js → Tooltip-DpDK7jWZ.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-YrRKtxsS.js → dist-B7BVkB7x.js} +1 -1
- package/dist/assets/web/dist/assets/{main-D6nzMf0k.js → main-BFkEOy3K.js} +2 -2
- package/dist/assets/web/dist/assets/main-DCH3Mezs.css +1 -0
- package/dist/cli.js +2336 -328
- package/package.json +2 -1
- package/dist/assets/web/dist/assets/RunRoutePage-Bu4CwEzt.js +0 -12
- package/dist/assets/web/dist/assets/main-CuENKPdD.css +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.213.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.213.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@anthropic-ai/sdk": "^0.124.0",
|
|
23
23
|
"@slack/bolt": "^5.1.0",
|
|
24
|
+
"aws4fetch": "^1.0.20",
|
|
24
25
|
"e2b": "^2.46.1",
|
|
25
26
|
"mdast-util-from-markdown": "^2.0.3",
|
|
26
27
|
"undici": "^8.10.2",
|
|
@@ -18999,11 +19000,12 @@
|
|
|
18999
19000
|
},
|
|
19000
19001
|
"packages/switchboard": {
|
|
19001
19002
|
"name": "@coreplane/switchboard",
|
|
19002
|
-
"version": "1.
|
|
19003
|
+
"version": "1.213.0",
|
|
19003
19004
|
"license": "Apache-2.0",
|
|
19004
19005
|
"dependencies": {
|
|
19005
19006
|
"@anthropic-ai/sdk": "^0.124.0",
|
|
19006
19007
|
"@slack/bolt": "^5.1.0",
|
|
19008
|
+
"aws4fetch": "^1.0.20",
|
|
19007
19009
|
"e2b": "^2.46.1",
|
|
19008
19010
|
"mdast-util-from-markdown": "^2.0.3",
|
|
19009
19011
|
"undici": "^8.10.2",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.213.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",
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@anthropic-ai/sdk": "^0.124.0",
|
|
83
83
|
"@slack/bolt": "^5.1.0",
|
|
84
|
+
"aws4fetch": "^1.0.20",
|
|
84
85
|
"e2b": "^2.46.1",
|
|
85
86
|
"mdast-util-from-markdown": "^2.0.3",
|
|
86
87
|
"undici": "^8.10.2",
|
package/dist/assets/source.json
CHANGED
|
@@ -42,6 +42,15 @@ export function machineNeedsRepo(machine: MachineClass): boolean {
|
|
|
42
42
|
export const IDENTITIES = ["none", "read", "write"] as const;
|
|
43
43
|
export type Identity = (typeof IDENTITIES)[number];
|
|
44
44
|
|
|
45
|
+
/** The loops a preset's runs can be driven by (docs/reference/specs/harness-pi.md
|
|
46
|
+
* item 1): `native`, the in-process turn loop (`src/runner.ts`), or `pi`, the
|
|
47
|
+
* pi coding agent in the run's own execution container, driven over its RPC
|
|
48
|
+
* protocol and bridged onto the run's events. A deployment's `harness:` block
|
|
49
|
+
* overrides a preset's own declaration (`effectiveHarness`,
|
|
50
|
+
* src/core/harness/select.ts). */
|
|
51
|
+
export const HARNESSES = ["native", "pi"] as const;
|
|
52
|
+
export type Harness = (typeof HARNESSES)[number];
|
|
53
|
+
|
|
45
54
|
/** The pace that marks a run as looping rather than working: a model turn
|
|
46
55
|
* every ten seconds, sustained for the whole wall clock. A busy run takes
|
|
47
56
|
* 20–40 s a turn (a model think plus a tool call), so a run that averages six
|
|
@@ -110,6 +119,11 @@ export interface AgentDef {
|
|
|
110
119
|
* discovery, no gh CLI. Selected by the dispatcher AFTER executor
|
|
111
120
|
* resolution via RunOptions.system; the shared AgentDef is never mutated. */
|
|
112
121
|
residentSystem?: string;
|
|
122
|
+
/** Which loop drives the preset's runs (`HARNESSES`): the native loop
|
|
123
|
+
* unless declared, and whatever a deployment's `harness.<preset>` says
|
|
124
|
+
* over that. Only a preset with a workspace can run on pi — pi is a process
|
|
125
|
+
* in the run's execution container. */
|
|
126
|
+
harness?: Harness;
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
// Every PR the coding agent ships carries a rich description by default —
|
|
@@ -435,6 +449,26 @@ Maintain the user-facing status card with the update_status tool: one item per c
|
|
|
435
449
|
|
|
436
450
|
Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack: lead with the outcome, then one line per child — its preset, its thread, its status and its result in a sentence — and what is still running, if anything.`;
|
|
437
451
|
|
|
452
|
+
/** The compound answer's preset — the one preset absent from the router's
|
|
453
|
+
* table that a plain message still reaches: a message with two or more
|
|
454
|
+
* independent asks routes to it with the parts named
|
|
455
|
+
* (docs/reference/specs/routing-and-config.md item 21). Its def below opts
|
|
456
|
+
* out of the table (`routable: false`); the router names it only in the
|
|
457
|
+
* compound form. */
|
|
458
|
+
export const COMPOUND_PRESET = "conductor";
|
|
459
|
+
|
|
460
|
+
/** How a plain message reaches a preset (docs/reference/specs/routing-and-config.md
|
|
461
|
+
* item 21), read off its def: `routed` — a row of the router's table, picked
|
|
462
|
+
* for a single ask; `compound` — the compound form alone, a message with
|
|
463
|
+
* several independent asks; `directive` — never picked, only `agent:<name>`.
|
|
464
|
+
* `help` renders its lines from this, so its words follow the registry. */
|
|
465
|
+
export type PresetDoor = "routed" | "compound" | "directive";
|
|
466
|
+
|
|
467
|
+
export function presetDoor(def: AgentDef): PresetDoor {
|
|
468
|
+
if (def.routable !== false) return "routed";
|
|
469
|
+
return def.name === COMPOUND_PRESET ? "compound" : "directive";
|
|
470
|
+
}
|
|
471
|
+
|
|
438
472
|
export const AGENTS: Record<string, AgentDef> = {
|
|
439
473
|
general: {
|
|
440
474
|
name: "general",
|
|
@@ -466,6 +500,9 @@ export const AGENTS: Record<string, AgentDef> = {
|
|
|
466
500
|
// `config set channel efforts.coding=…`, or `effort:` per request).
|
|
467
501
|
machine: "repo-resident",
|
|
468
502
|
identity: "write", // pushes branches and opens pull requests
|
|
503
|
+
// The native loop until the pi series moves this preset; a deployment
|
|
504
|
+
// flips it early with `harness: { coding: pi }` (docs/reference/specs/harness-pi.md).
|
|
505
|
+
harness: "native",
|
|
469
506
|
},
|
|
470
507
|
review: {
|
|
471
508
|
name: "review",
|
|
@@ -46,6 +46,14 @@ const REDACT: Array<{ re: RegExp; replace: string }> = [
|
|
|
46
46
|
const SECRET_COMPONENT =
|
|
47
47
|
/^(secret|token|password|passwd|pwd|credential|credentials|key|apikey|auth|session|sessionid|cookie)$/i;
|
|
48
48
|
|
|
49
|
+
// An identifier component that marks the whole identifier as an ERROR CODE, not
|
|
50
|
+
// a secret name: `github-token-mint-failed: HTTP 422 …` names a failure about a
|
|
51
|
+
// token, and the word after the colon is diagnosis, never a credential. Without
|
|
52
|
+
// this guard the assignment pass redacted `HTTP` out of that message
|
|
53
|
+
// (`github-token-mint-failed: «redacted» 422 …`), garbling the one line that
|
|
54
|
+
// tells an operator what went wrong.
|
|
55
|
+
const ERROR_CODE_COMPONENT = /^(failed|failure|error|err|refused|denied|invalid|missing|expired|unset|mismatch)$/i;
|
|
56
|
+
|
|
49
57
|
/** Redact the VALUE of any `<name> = value` / `<name>: value` where the name has
|
|
50
58
|
* a secret-marking component. Handles quoted values (with spaces) and unquoted,
|
|
51
59
|
* and a quoted NAME (`"password": "…"` in pasted JSON — the closing quote sits
|
|
@@ -64,7 +72,9 @@ function redactNamedAssignments(text: string): string {
|
|
|
64
72
|
// decides whether it names a secret (`_SECRET` → ["", "SECRET"]).
|
|
65
73
|
/(?<![A-Za-z0-9_-])([A-Za-z0-9_-]+)("?)(\s*[=:]\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s]{4,})/g,
|
|
66
74
|
(whole, id: string, close: string, sep: string, val: string) => {
|
|
67
|
-
|
|
75
|
+
const parts = id.split(/[_-]/);
|
|
76
|
+
if (!parts.some((p) => SECRET_COMPONENT.test(p))) return whole;
|
|
77
|
+
if (parts.some((p) => ERROR_CODE_COMPONENT.test(p))) return whole;
|
|
68
78
|
const quote = val[0] === '"' || val[0] === "'" ? val[0] : "";
|
|
69
79
|
return `${id}${close}${sep}${quote}«redacted»${quote}`;
|
|
70
80
|
},
|
|
@@ -118,7 +118,23 @@ export type RunNoteKind =
|
|
|
118
118
|
* post-step beside the thread's Slack-only note, so the record says the
|
|
119
119
|
* verdict is Slack-only and a coordinator reading it never asks GitHub
|
|
120
120
|
* for a review that was never sent. */
|
|
121
|
-
| "review_not_posted"
|
|
121
|
+
| "review_not_posted"
|
|
122
|
+
/** A pi run's context was compacted (docs/reference/specs/harness-pi.md item
|
|
123
|
+
* 6): pi summarized its older turns into one entry and the model reads the
|
|
124
|
+
* summary from here on; the transcript keeps the originals, so the record
|
|
125
|
+
* is a superset of the model's context. The summary names the token counts
|
|
126
|
+
* before and after. Published by the pi bridge. */
|
|
127
|
+
| "compacted"
|
|
128
|
+
/** The pi harness itself failed in a way the run must show (harness-pi.md
|
|
129
|
+
* item 6): the extension threw, pi asked a dialog no one answers (answered
|
|
130
|
+
* cancelled), or pi emitted an event kind this build's bridge does not
|
|
131
|
+
* know — named, so a pi bump is visible in the first run's record. Published
|
|
132
|
+
* by the pi bridge. */
|
|
133
|
+
| "harness_error"
|
|
134
|
+
/** The harness's gate refused a tool call the model asked for (harness-pi.md
|
|
135
|
+
* item 7): the summary names the tool and the rule; the model read the same
|
|
136
|
+
* reason as the tool's result. Published by the bot's authorize route. */
|
|
137
|
+
| "tool_refused";
|
|
122
138
|
|
|
123
139
|
/** Every `RunNoteKind`, as a value (a reader that filters notes by kind uses
|
|
124
140
|
* this; adding a kind to the union without adding it here is a type error). */
|
|
@@ -139,6 +155,9 @@ export const RUN_NOTE_KINDS = [
|
|
|
139
155
|
"cold_sandbox",
|
|
140
156
|
"pr_not_opened",
|
|
141
157
|
"review_not_posted",
|
|
158
|
+
"compacted",
|
|
159
|
+
"harness_error",
|
|
160
|
+
"tool_refused",
|
|
142
161
|
] as const satisfies readonly RunNoteKind[];
|
|
143
162
|
type _EveryKindListed = [RunNoteKind] extends [(typeof RUN_NOTE_KINDS)[number]] ? true : never;
|
|
144
163
|
const _everyKindListed: _EveryKindListed = true;
|
|
@@ -383,6 +402,27 @@ export type RunEvent =
|
|
|
383
402
|
seq?: number;
|
|
384
403
|
at?: number;
|
|
385
404
|
}
|
|
405
|
+
/** A file moved through the artifact store (docs/reference/specs/execution.md item 20,
|
|
406
|
+
* record 0033): one the run received from its thread (`in`, staged before
|
|
407
|
+
* the turn) or one it sent (`out`, `attach_file`). The record keeps the
|
|
408
|
+
* store KEY and the facts a page needs to list the file — never a URL: the
|
|
409
|
+
* run page's proxy route (`/runs/:id/artifacts/<key>`, live-view.md item 26)
|
|
410
|
+
* mints a signed GET per request, so a stored record never carries a
|
|
411
|
+
* credential that expires or leaks, and the parser refuses a payload that
|
|
412
|
+
* tries. A side fact beside the tool pair that moved the file (like
|
|
413
|
+
* `skill_use`), never a step; the friction analyzer ignores it. */
|
|
414
|
+
| {
|
|
415
|
+
type: "artifact";
|
|
416
|
+
direction: "in" | "out";
|
|
417
|
+
/** The store key (`src/artifacts/keys.ts`): `runs/<runId>/out/<seq>-<basename>` or `threads/<thread>/in/<ts>/<i>-<basename>`. */
|
|
418
|
+
key: string;
|
|
419
|
+
/** The file's name as the person sees it (the Slack filename, the tool's `name`). */
|
|
420
|
+
name: string;
|
|
421
|
+
size: number;
|
|
422
|
+
contentType: string;
|
|
423
|
+
seq?: number;
|
|
424
|
+
at?: number;
|
|
425
|
+
}
|
|
386
426
|
/** A skill was loaded into the model's context (docs/reference/specs/skills.md). Emitted
|
|
387
427
|
* by the `use_skill` tool on a successful load — alongside, not instead of,
|
|
388
428
|
* its `tool_call`/`tool_result` pair — so skill use is a first-class fact in
|
|
@@ -384,12 +384,14 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
384
384
|
return;
|
|
385
385
|
}
|
|
386
386
|
// Side facts about the run, not steps: skill_use rides beside a use_skill
|
|
387
|
-
// call that already produced its own tool pair
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
387
|
+
// call that already produced its own tool pair, and artifact beside the
|
|
388
|
+
// attach_file call (or the dispatcher's staging) that moved the file;
|
|
389
|
+
// review_artifact, pr_description, pr_opened, review_posted, the ship_round
|
|
390
|
+
// boundaries and the router's route are published by the dispatcher/pipeline
|
|
391
|
+
// outside the model loop entirely. Counting any of them would distort the story.
|
|
391
392
|
if (
|
|
392
393
|
ev.type === "skill_use" ||
|
|
394
|
+
ev.type === "artifact" ||
|
|
393
395
|
ev.type === "review_artifact" ||
|
|
394
396
|
ev.type === "pr_description" ||
|
|
395
397
|
ev.type === "pr_opened" ||
|
|
@@ -55,6 +55,10 @@ export function shimRoute(pathname: string): string | undefined {
|
|
|
55
55
|
// The model proxy's two routes (docs/reference/specs/model-proxy.md): a bounded
|
|
56
56
|
// request per model call, forwarded to the container like everything else.
|
|
57
57
|
if (pathname === "/v1/messages" || pathname === "/v1/chat/completions") return "model-proxy";
|
|
58
|
+
// The pi harness's three routes (docs/reference/specs/harness-pi.md item 7): a
|
|
59
|
+
// run's extension asking for its tools, a verdict, a relayed tool's result.
|
|
60
|
+
if (pathname === "/harness/tools" || pathname === "/harness/authorize" || pathname === "/harness/tool")
|
|
61
|
+
return "harness";
|
|
58
62
|
if (pathname === "/docs" || pathname.startsWith("/docs/")) return "docs";
|
|
59
63
|
if (pathname === "/" || pathname === "/index.html") return "page";
|
|
60
64
|
return "other";
|
|
@@ -117,6 +117,10 @@ export function checkoutUpdateCommand(sha: string, clean: CleanScope): string {
|
|
|
117
117
|
* step at all.) The kill surfaces either as the shell's
|
|
118
118
|
* own death (SIGTERM, exit 143, "Session terminated") or, past the shell, as
|
|
119
119
|
* the SDK's replacement errors (stale process handle, closed supervisor).
|
|
120
|
+
* `runtimeUnreachable` is the other outcome that says nothing about the
|
|
121
|
+
* repository: the container's control port never answered the SDK's connect
|
|
122
|
+
* (`runtimeUnreachableReason`), so no command ran at all — the resident
|
|
123
|
+
* escalates on its persisted count instead of recording a command failure.
|
|
120
124
|
* Everything else is the repo's own build failing. */
|
|
121
125
|
export interface RefreshFailure {
|
|
122
126
|
/** The reason. An interruption's is prefixed `refresh-interrupted:` and is
|
|
@@ -124,10 +128,13 @@ export interface RefreshFailure {
|
|
|
124
128
|
* whose retry re-enters the step, and the row keeps its state (the word
|
|
125
129
|
* shows on the instance row's `lastStep` and in the log). A full disk is
|
|
126
130
|
* `disk-full:` (`residentDisk.ts` — the cycle's entry gate and the recycle
|
|
127
|
-
* decision key on it);
|
|
131
|
+
* decision key on it); an unanswered control port is `runtime-unreachable:`
|
|
132
|
+
* with the attempt count (the ladder below decides what the resident does
|
|
133
|
+
* about it); real failures keep `<step>-failed:`. */
|
|
128
134
|
reason: string;
|
|
129
135
|
interrupted: boolean;
|
|
130
136
|
diskFull: boolean;
|
|
137
|
+
runtimeUnreachable: boolean;
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
/** Root argv that kills every process the build user still owns and waits
|
|
@@ -202,7 +209,16 @@ const INTERRUPTION_SIGNATURE = /\bexit 143\b|Session terminated|SIGTERM/;
|
|
|
202
209
|
export const RUNTIME_REPLACEMENT_WORDING =
|
|
203
210
|
/previous runtime incarnation|interrupted because the runtime changed|runtime identity is no longer active|sandbox lifetime is no longer current|platform was updating the sandbox runtime|no longer identifies pid|process supervisor is closed|container is not running, consider calling start/i;
|
|
204
211
|
|
|
205
|
-
/** `
|
|
212
|
+
/** `runtimeUnreachable` is the caller's word that the failure was the SDK's
|
|
213
|
+
* connect abort at the exec choke point (the Worker's typed check over the
|
|
214
|
+
* error and its cause chain, `isRuntimeUnreachableSignal`) with the count it
|
|
215
|
+
* persisted: the outcome is decided before anything else — no command ran, so
|
|
216
|
+
* neither the message nor a disk probe is evidence — and the reason is the
|
|
217
|
+
* named one, never the SDK's bare `The operation was aborted`. Without that
|
|
218
|
+
* word the classifier does not guess from the wording: an abort message on
|
|
219
|
+
* its own stays the step's failure, exactly as it did before the ladder.
|
|
220
|
+
*
|
|
221
|
+
* `freeKiB` is the `df` probe's answer (`parseDfFreeKiB`), taken AFTER the
|
|
206
222
|
* step failed and only consulted when the message itself carries no errno:
|
|
207
223
|
* a message saying ENOSPC is disk-full outright (the disk is the actionable
|
|
208
224
|
* fact, whatever else the message says); a kill signature without it is an
|
|
@@ -213,23 +229,153 @@ export function classifyRefreshFailure(input: {
|
|
|
213
229
|
step: string;
|
|
214
230
|
message: string;
|
|
215
231
|
freeKiB?: number | null;
|
|
232
|
+
runtimeUnreachable?: { count: number };
|
|
216
233
|
}): RefreshFailure {
|
|
217
234
|
const { step, message } = input;
|
|
235
|
+
if (input.runtimeUnreachable) {
|
|
236
|
+
return {
|
|
237
|
+
interrupted: false,
|
|
238
|
+
diskFull: false,
|
|
239
|
+
runtimeUnreachable: true,
|
|
240
|
+
reason: runtimeUnreachableReason(input.runtimeUnreachable.count),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
218
243
|
if (isDiskFullMessage(message)) {
|
|
219
244
|
return {
|
|
220
245
|
interrupted: false,
|
|
221
246
|
diskFull: true,
|
|
247
|
+
runtimeUnreachable: false,
|
|
222
248
|
reason: diskFullReason({ step, message, freeKiB: input.freeKiB ?? null }),
|
|
223
249
|
};
|
|
224
250
|
}
|
|
225
251
|
const timedOut = /\(timed out\)/.test(message);
|
|
226
252
|
if (!timedOut && (INTERRUPTION_SIGNATURE.test(message) || RUNTIME_REPLACEMENT_WORDING.test(message))) {
|
|
227
|
-
return {
|
|
253
|
+
return {
|
|
254
|
+
interrupted: true,
|
|
255
|
+
diskFull: false,
|
|
256
|
+
runtimeUnreachable: false,
|
|
257
|
+
reason: `refresh-interrupted: ${step} ${message}`,
|
|
258
|
+
};
|
|
228
259
|
}
|
|
229
260
|
if (input.freeKiB !== undefined && input.freeKiB !== null && input.freeKiB < DISK_FULL_FREE_KIB) {
|
|
230
|
-
return {
|
|
261
|
+
return {
|
|
262
|
+
interrupted: false,
|
|
263
|
+
diskFull: true,
|
|
264
|
+
runtimeUnreachable: false,
|
|
265
|
+
reason: diskFullReason({ step, message, freeKiB: input.freeKiB }),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return { interrupted: false, diskFull: false, runtimeUnreachable: false, reason: `${step}-failed: ${message}` };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// -- the runtime that never answers --------------------------------------------
|
|
272
|
+
|
|
273
|
+
/** The Sandbox SDK's connect timeout (`DEFAULT_CONNECT_TIMEOUT_MS` in its
|
|
274
|
+
* container-control connection, @cloudflare/sandbox@0.13.0-next.751.1): how
|
|
275
|
+
* long ONE WebSocket upgrade to the container's control port 3000 may take
|
|
276
|
+
* before the SDK aborts it. A module constant there, not an option — the
|
|
277
|
+
* 30 s is the SDK's, not ours; pinned by a test that reads the installed
|
|
278
|
+
* dist, so an SDK bump that moves it fails the build, not a reason string. */
|
|
279
|
+
export const SDK_CONNECT_TIMEOUT_MS = 30_000;
|
|
280
|
+
|
|
281
|
+
/** The SDK's env knob for how long the physical start waits for the control
|
|
282
|
+
* port to accept a request before the first connect (`portReadyTimeoutMS`,
|
|
283
|
+
* read from this var, 10 s to 600 s; its default is 90 s). The wake path's
|
|
284
|
+
* first exec after a start is bounded by that wait PLUS one connect timeout:
|
|
285
|
+
* a slow boot (a 2.65 GB image on a cold host) spends its time in the port
|
|
286
|
+
* wait, so a start given `WAKE_PORT_READY_MS` is not mistaken for an
|
|
287
|
+
* unreachable runtime. The value lives in the resident's wrangler template
|
|
288
|
+
* (`vars`), pinned to the constant by a test. */
|
|
289
|
+
export const SDK_PORT_READY_ENV = "SANDBOX_PORT_TIMEOUT_MS";
|
|
290
|
+
export const WAKE_PORT_READY_MS = 3 * 60_000;
|
|
291
|
+
|
|
292
|
+
/** The DO storage key the SDK keeps its runtime identity under
|
|
293
|
+
* (`RUNTIME_RECORD_KEY` in its runtime lifecycle; not exported). The
|
|
294
|
+
* resident forgets it before it destroys a container: the SDK's `destroy()`
|
|
295
|
+
* skips its runtime cleanup when no identity is stored — against a silent
|
|
296
|
+
* control port that cleanup would only wait out its bound — and a destroy
|
|
297
|
+
* that fails midway leaves no identity behind to be re-adopted. `stop()` and
|
|
298
|
+
* `destroy()` delete it themselves (`invalidate`); the explicit delete is the
|
|
299
|
+
* guard for the path where they do not get that far. Pinned to the installed
|
|
300
|
+
* SDK by a test. */
|
|
301
|
+
export const SDK_RUNTIME_RECORD_KEY = "currentRuntimeIdentity";
|
|
302
|
+
|
|
303
|
+
/** The DOMException the SDK's connect abort raises: `AbortError`, message
|
|
304
|
+
* `The operation was aborted`. Anchored on the whole sentence so a command's
|
|
305
|
+
* own `Aborted (core dumped)` never matches. */
|
|
306
|
+
export const RUNTIME_UNREACHABLE_WORDING = /\bThe operation was aborted\b/;
|
|
307
|
+
|
|
308
|
+
/** Is this one link of an error's cause chain the SDK's connect abort? By
|
|
309
|
+
* name first (the DOMException's `AbortError`), by the DOMException's message
|
|
310
|
+
* when a wrapper copied only that. Applied by the Worker to the SPAWN-phase
|
|
311
|
+
* error alone — after its replacement check, since a replaced runtime is a
|
|
312
|
+
* different fact (`RUNTIME_REPLACEMENT_WORDING`, disjoint by construction) —
|
|
313
|
+
* never to a command's own output, which is a `StepError` with an exit code. */
|
|
314
|
+
export function isRuntimeUnreachableSignal(link: unknown): boolean {
|
|
315
|
+
if (link === null || typeof link !== "object") return false;
|
|
316
|
+
const { name, message } = link as { name?: unknown; message?: unknown };
|
|
317
|
+
if (name === "AbortError") return true;
|
|
318
|
+
return typeof message === "string" && RUNTIME_UNREACHABLE_WORDING.test(message);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The escalation ladder over the resident's persisted count of consecutive
|
|
322
|
+
* unanswered connects (the row is cleared by any exec whose process spawned).
|
|
323
|
+
* Each rung is what the refresh instance does when a step's exec finds the
|
|
324
|
+
* control port silent AGAIN at that count:
|
|
325
|
+
* re-arm (1–2) `degraded(runtime-unreachable …)`, the step thrown to the
|
|
326
|
+
* engine whose retry re-enters it thirty seconds on — the
|
|
327
|
+
* short re-arm a `refresh-interrupted` gets;
|
|
328
|
+
* stop (3) the container is sent SIGTERM (`stop()`), the incarnation
|
|
329
|
+
* swapped, the retry starts it again — cheap, and enough
|
|
330
|
+
* for a runtime that still honours signals;
|
|
331
|
+
* recreate (4–5) the SDK's runtime identity is forgotten
|
|
332
|
+
* (`SDK_RUNTIME_RECORD_KEY`) and the VM destroyed
|
|
333
|
+
* (`destroy()`: SIGKILL, a fresh disk — where `stop()` is a
|
|
334
|
+
* SIGTERM a wedged runtime ignores, as the incident's admin
|
|
335
|
+
* stop showed), every snapshot and entry backup kept, the
|
|
336
|
+
* incarnation swapped; the retry's wake path finds no
|
|
337
|
+
* runtime, flips `restoring` and restores mirror, checkout
|
|
338
|
+
* and deps from R2 — minutes, not the thirty-minute rebuild;
|
|
339
|
+
* down (≥ 6) a recreated container did not answer either: the resident
|
|
340
|
+
* goes `down(runtime-unreachable …)`, a reason the watchdog's
|
|
341
|
+
* auto-rebuild strikes apply to, so the rebuild — destroy
|
|
342
|
+
* plus reprovision from the code host — is the last resort,
|
|
343
|
+
* not the first. */
|
|
344
|
+
export const RUNTIME_UNREACHABLE_STOP_AT = 3;
|
|
345
|
+
export const RUNTIME_UNREACHABLE_RECREATE_AT = 4;
|
|
346
|
+
export const RUNTIME_UNREACHABLE_DOWN_AT = 6;
|
|
347
|
+
|
|
348
|
+
export type RuntimeUnreachableRung = "re-arm" | "stop" | "recreate" | "down";
|
|
349
|
+
|
|
350
|
+
export function runtimeUnreachableRung(count: number): RuntimeUnreachableRung {
|
|
351
|
+
if (!Number.isFinite(count) || count < 1) {
|
|
352
|
+
throw new RangeError(`runtime-unreachable rung needs a count of at least 1, got ${count}`);
|
|
231
353
|
}
|
|
232
|
-
|
|
354
|
+
if (count >= RUNTIME_UNREACHABLE_DOWN_AT) return "down";
|
|
355
|
+
if (count >= RUNTIME_UNREACHABLE_RECREATE_AT) return "recreate";
|
|
356
|
+
if (count >= RUNTIME_UNREACHABLE_STOP_AT) return "stop";
|
|
357
|
+
return "re-arm";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const RUNTIME_UNREACHABLE_ACTION: Readonly<Record<RuntimeUnreachableRung, string>> = {
|
|
361
|
+
"re-arm": "the step is retried",
|
|
362
|
+
stop: "the container was stopped (SIGTERM); the retry starts it again",
|
|
363
|
+
recreate:
|
|
364
|
+
"the container was destroyed and the SDK's runtime identity forgotten, snapshots kept; the retry restores the checkout from the snapshot",
|
|
365
|
+
down: "a recreated container did not answer either; only a rebuild follows",
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/** The reason a silent control port is recorded under: the port, the SDK's
|
|
369
|
+
* connect timeout and the attempt out of the down threshold — and, when a
|
|
370
|
+
* rung acted, what it did and what the retry does next. Never the SDK's bare
|
|
371
|
+
* `The operation was aborted`. */
|
|
372
|
+
export function runtimeUnreachableReason(count: number, rung?: RuntimeUnreachableRung): string {
|
|
373
|
+
const base = `runtime-unreachable: the container's control port did not answer within ${SDK_CONNECT_TIMEOUT_MS / 1000} s (attempt ${count} of ${RUNTIME_UNREACHABLE_DOWN_AT})`;
|
|
374
|
+
return rung ? `${base} — ${RUNTIME_UNREACHABLE_ACTION[rung]}` : base;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function isRuntimeUnreachableReason(reason: string): boolean {
|
|
378
|
+
return /^runtime-unreachable: /.test(reason);
|
|
233
379
|
}
|
|
234
380
|
|
|
235
381
|
/** What the wake path does with a failed restore. `down` is the rule: the
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"src": "../node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-normal.woff2"
|
|
29
29
|
},
|
|
30
30
|
"../node_modules/@pierre/diffs/dist/index.js": {
|
|
31
|
-
"file": "assets/dist-
|
|
31
|
+
"file": "assets/dist-B7BVkB7x.js",
|
|
32
32
|
"name": "dist",
|
|
33
33
|
"src": "../node_modules/@pierre/diffs/dist/index.js",
|
|
34
34
|
"isDynamicEntry": true,
|
|
@@ -2562,16 +2562,16 @@
|
|
|
2562
2562
|
"_seed-DeKSHrt2.js"
|
|
2563
2563
|
]
|
|
2564
2564
|
},
|
|
2565
|
-
"_StatusDot-
|
|
2566
|
-
"file": "assets/StatusDot-
|
|
2565
|
+
"_StatusDot-D8Wwt1KC.js": {
|
|
2566
|
+
"file": "assets/StatusDot-D8Wwt1KC.js",
|
|
2567
2567
|
"name": "StatusDot",
|
|
2568
2568
|
"imports": [
|
|
2569
2569
|
"_seed-DeKSHrt2.js",
|
|
2570
|
-
"_Tooltip-
|
|
2570
|
+
"_Tooltip-DpDK7jWZ.js"
|
|
2571
2571
|
]
|
|
2572
2572
|
},
|
|
2573
|
-
"_Tooltip-
|
|
2574
|
-
"file": "assets/Tooltip-
|
|
2573
|
+
"_Tooltip-DpDK7jWZ.js": {
|
|
2574
|
+
"file": "assets/Tooltip-DpDK7jWZ.js",
|
|
2575
2575
|
"name": "Tooltip",
|
|
2576
2576
|
"imports": [
|
|
2577
2577
|
"_AppShell-lYVcj-k7.js",
|
|
@@ -2789,7 +2789,7 @@
|
|
|
2789
2789
|
]
|
|
2790
2790
|
},
|
|
2791
2791
|
"src/main.ts": {
|
|
2792
|
-
"file": "assets/main-
|
|
2792
|
+
"file": "assets/main-BFkEOy3K.js",
|
|
2793
2793
|
"name": "main",
|
|
2794
2794
|
"src": "src/main.ts",
|
|
2795
2795
|
"isEntry": true,
|
|
@@ -2807,7 +2807,7 @@
|
|
|
2807
2807
|
"_NotFoundPage-BzWS4Ca1.js"
|
|
2808
2808
|
],
|
|
2809
2809
|
"css": [
|
|
2810
|
-
"assets/main-
|
|
2810
|
+
"assets/main-DCH3Mezs.css"
|
|
2811
2811
|
],
|
|
2812
2812
|
"assets": [
|
|
2813
2813
|
"assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2",
|
|
@@ -2841,19 +2841,19 @@
|
|
|
2841
2841
|
]
|
|
2842
2842
|
},
|
|
2843
2843
|
"src/pages/ResidentDetailPage.vue": {
|
|
2844
|
-
"file": "assets/ResidentDetailPage-
|
|
2844
|
+
"file": "assets/ResidentDetailPage-CvwjhjlG.js",
|
|
2845
2845
|
"name": "ResidentDetailPage",
|
|
2846
2846
|
"src": "src/pages/ResidentDetailPage.vue",
|
|
2847
2847
|
"isDynamicEntry": true,
|
|
2848
2848
|
"imports": [
|
|
2849
2849
|
"_AppShell-lYVcj-k7.js",
|
|
2850
2850
|
"_seed-DeKSHrt2.js",
|
|
2851
|
-
"_StatusDot-
|
|
2851
|
+
"_StatusDot-D8Wwt1KC.js",
|
|
2852
2852
|
"_residentDiskBudget-BMBKlYRH.js"
|
|
2853
2853
|
]
|
|
2854
2854
|
},
|
|
2855
2855
|
"src/pages/ResidentsIndexPage.vue": {
|
|
2856
|
-
"file": "assets/ResidentsIndexPage-
|
|
2856
|
+
"file": "assets/ResidentsIndexPage-DoN6XuNx.js",
|
|
2857
2857
|
"name": "ResidentsIndexPage",
|
|
2858
2858
|
"src": "src/pages/ResidentsIndexPage.vue",
|
|
2859
2859
|
"isDynamicEntry": true,
|
|
@@ -2861,20 +2861,20 @@
|
|
|
2861
2861
|
"_AppShell-lYVcj-k7.js",
|
|
2862
2862
|
"_seed-DeKSHrt2.js",
|
|
2863
2863
|
"_wallClock-61NpVkY6.js",
|
|
2864
|
-
"_StatusDot-
|
|
2864
|
+
"_StatusDot-D8Wwt1KC.js",
|
|
2865
2865
|
"_residentDiskBudget-BMBKlYRH.js",
|
|
2866
2866
|
"_indexFormat-B-pd4r7Y.js"
|
|
2867
2867
|
]
|
|
2868
2868
|
},
|
|
2869
2869
|
"src/pages/RunRoutePage.vue": {
|
|
2870
|
-
"file": "assets/RunRoutePage-
|
|
2870
|
+
"file": "assets/RunRoutePage-FzLNexjF.js",
|
|
2871
2871
|
"name": "RunRoutePage",
|
|
2872
2872
|
"src": "src/pages/RunRoutePage.vue",
|
|
2873
2873
|
"isDynamicEntry": true,
|
|
2874
2874
|
"imports": [
|
|
2875
2875
|
"_AppShell-lYVcj-k7.js",
|
|
2876
2876
|
"_seed-DeKSHrt2.js",
|
|
2877
|
-
"_Tooltip-
|
|
2877
|
+
"_Tooltip-DpDK7jWZ.js",
|
|
2878
2878
|
"src/main.ts",
|
|
2879
2879
|
"_wallClock-61NpVkY6.js",
|
|
2880
2880
|
"_NotFoundPage-BzWS4Ca1.js",
|
|
@@ -2890,31 +2890,31 @@
|
|
|
2890
2890
|
]
|
|
2891
2891
|
},
|
|
2892
2892
|
"src/pages/RunsIndexPage.vue": {
|
|
2893
|
-
"file": "assets/RunsIndexPage-
|
|
2893
|
+
"file": "assets/RunsIndexPage-DhSUbF9k.js",
|
|
2894
2894
|
"name": "RunsIndexPage",
|
|
2895
2895
|
"src": "src/pages/RunsIndexPage.vue",
|
|
2896
2896
|
"isDynamicEntry": true,
|
|
2897
2897
|
"imports": [
|
|
2898
2898
|
"_AppShell-lYVcj-k7.js",
|
|
2899
2899
|
"_seed-DeKSHrt2.js",
|
|
2900
|
-
"_Tooltip-
|
|
2900
|
+
"_Tooltip-DpDK7jWZ.js",
|
|
2901
2901
|
"_wallClock-61NpVkY6.js",
|
|
2902
2902
|
"_RunsTabs-DEICQ4FY.js",
|
|
2903
|
-
"_StatusDot-
|
|
2903
|
+
"_StatusDot-D8Wwt1KC.js",
|
|
2904
2904
|
"_indexFormat-B-pd4r7Y.js",
|
|
2905
2905
|
"_favicon-CWPcvWvp.js",
|
|
2906
2906
|
"_localIso-L06jV29p.js"
|
|
2907
2907
|
]
|
|
2908
2908
|
},
|
|
2909
2909
|
"src/pages/ScheduledPage.vue": {
|
|
2910
|
-
"file": "assets/ScheduledPage-
|
|
2910
|
+
"file": "assets/ScheduledPage-Bq0Yg4nT.js",
|
|
2911
2911
|
"name": "ScheduledPage",
|
|
2912
2912
|
"src": "src/pages/ScheduledPage.vue",
|
|
2913
2913
|
"isDynamicEntry": true,
|
|
2914
2914
|
"imports": [
|
|
2915
2915
|
"_AppShell-lYVcj-k7.js",
|
|
2916
2916
|
"_seed-DeKSHrt2.js",
|
|
2917
|
-
"_Tooltip-
|
|
2917
|
+
"_Tooltip-DpDK7jWZ.js",
|
|
2918
2918
|
"_wallClock-61NpVkY6.js",
|
|
2919
2919
|
"_RunsTabs-DEICQ4FY.js",
|
|
2920
2920
|
"_indexFormat-B-pd4r7Y.js",
|