@coreplane/switchboard 1.255.0 → 1.257.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 +14 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +340 -7
- package/dist/assets/deploy/cloudflare-resident/drain.ts +100 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +227 -34
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +5 -3
- package/dist/assets/project.json +17 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +55 -1
- package/dist/assets/src/core/authz/grants.ts +12 -0
- package/dist/assets/src/core/authz/policy.ts +15 -0
- package/dist/assets/src/core/budgets.ts +51 -17
- package/dist/assets/src/core/coordinator/contract.ts +45 -5
- package/dist/assets/src/core/coordinator/driver.ts +17 -4
- package/dist/assets/src/core/delivery.ts +8 -5
- package/dist/assets/src/core/pipelineStanding.ts +57 -0
- package/dist/assets/src/core/plane/decide.ts +468 -12
- package/dist/assets/src/core/plane/findings.ts +120 -0
- package/dist/assets/src/core/reviewVerdict.ts +49 -0
- package/dist/assets/src/core/runEvents.ts +41 -14
- package/dist/assets/src/core/runFriction.ts +16 -0
- package/dist/assets/src/core/runLedger/types.ts +16 -3
- package/dist/assets/src/core/runRecord.ts +12 -0
- package/dist/assets/src/core/ship/contract.ts +34 -32
- package/dist/assets/src/core/ship/coordinator.ts +291 -95
- package/dist/assets/src/core/ship/renewal.ts +19 -17
- package/dist/assets/src/core/trace/attrs.ts +1 -1
- package/dist/assets/src/core/untrusted.ts +35 -0
- package/dist/assets/src/execution/residentCredentials.ts +7 -4
- package/dist/assets/web/dist/.vite/manifest.json +82 -57
- package/dist/assets/web/dist/assets/HomePage-DJqJQzrL.js +1 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-fhKqKUVv.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-Bood0hNO.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-D_RD6wLo.js → ResidentDetailPage-DOe1HtUC.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BLkSuCxo.js → ResidentsIndexPage-TyCGjXM4.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-C_4t-sit.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BNepPMRX.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-jZnMw1mM.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-3aYsDf-q.js → ScheduledPage-BkNoudDP.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-DG-p5Xy1.js → SettingsPage-COvWGXl-.js} +1 -1
- package/dist/assets/web/dist/assets/SilentTurn-DNAANKgN.js +2 -0
- package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-CEnGlyAL.js → StatusDot-BgmkSV0S.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CiunVowT.js → Tooltip-DEUPuCPW.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DNhdzxvY.js +1 -0
- package/dist/assets/web/dist/assets/{dist-luhv3YSo.js → dist-CF3jz9LM.js} +1 -1
- package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
- package/dist/assets/web/dist/assets/main-7SAujY_s.js +28 -0
- package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
- package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BRJEIh83.js} +1 -1
- package/dist/cli.js +2941 -1116
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-Be7jLLnU.js +0 -3
- package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CmWYGR36.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DW-HHuZa.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
- package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
- package/dist/assets/web/dist/assets/main-mAKx_zo9.js +0 -28
|
@@ -6,6 +6,98 @@
|
|
|
6
6
|
// at all. Node-free by design, like `table.ts`: no clock, no io, no ids it
|
|
7
7
|
// did not derive from its inputs — the same event over the same state is the
|
|
8
8
|
// same answer, which is what the shadow comparison and the tests rest on.
|
|
9
|
+
// (`budgets.ts` is constants, not io — the one import keeps every duration
|
|
10
|
+
// literal where `clock:check` expects it.)
|
|
11
|
+
|
|
12
|
+
import { PLANE, minutesToMs } from "../budgets.js";
|
|
13
|
+
import type { PlaneFinding } from "./findings.js";
|
|
14
|
+
|
|
15
|
+
// ---- endings and their causes (record 0064, "Endings and the watches") ------------------------
|
|
16
|
+
|
|
17
|
+
/** The closed set an ending's cause comes from (record 0064): the plane
|
|
18
|
+
* assigns it because it is the only component that saw both the lease and
|
|
19
|
+
* the generation; the bot's notes, the reattach text and the runner's report
|
|
20
|
+
* render it and never compose one. */
|
|
21
|
+
export type PlaneEndingCause =
|
|
22
|
+
"completed" | "failed" | "stopped" | "withdrawn" | "refused" | "lease_lapsed" | "resident_replaced" | "runner_gone";
|
|
23
|
+
|
|
24
|
+
export const PLANE_ENDING_CAUSES: readonly PlaneEndingCause[] = [
|
|
25
|
+
"completed",
|
|
26
|
+
"failed",
|
|
27
|
+
"stopped",
|
|
28
|
+
"withdrawn",
|
|
29
|
+
"refused",
|
|
30
|
+
"lease_lapsed",
|
|
31
|
+
"resident_replaced",
|
|
32
|
+
"runner_gone",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** The ending fact the object records when a live row closes: the record's
|
|
36
|
+
* own kind (its status word) and the cause from the closed set. One per
|
|
37
|
+
* closed row — the first cause stands, except that the same run's later
|
|
38
|
+
* finish replaces a standing `resident_replaced`: a restarting close is the
|
|
39
|
+
* run continuing under its own id, not its end. */
|
|
40
|
+
export interface PlaneEnding {
|
|
41
|
+
kind: string;
|
|
42
|
+
cause: PlaneEndingCause;
|
|
43
|
+
at: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** How the bot's reclaim classified one row (record 0064): `resume`, `restart`
|
|
47
|
+
* and `rehost` continue the run, so the plane records nothing for them; only
|
|
48
|
+
* `closed` assigns a cause. */
|
|
49
|
+
export type PlaneReclaimWord = "resume" | "restart" | "rehost" | "closed";
|
|
50
|
+
|
|
51
|
+
/** The cause a closing record's status maps to (record 0064). An `interrupted`
|
|
52
|
+
* close with `restarting` set is the reattach path restarting the run after
|
|
53
|
+
* its workspace vanished — the resident's container was replaced under it;
|
|
54
|
+
* any other `interrupted` close falls to `lease_lapsed`, which is true and
|
|
55
|
+
* blames nobody. */
|
|
56
|
+
export function causeOfClose(status: string, restarting?: boolean): PlaneEndingCause {
|
|
57
|
+
switch (status) {
|
|
58
|
+
case "completed":
|
|
59
|
+
return "completed";
|
|
60
|
+
case "failed":
|
|
61
|
+
return "failed";
|
|
62
|
+
case "stopped_soft":
|
|
63
|
+
case "stopped_hard":
|
|
64
|
+
case "stopped":
|
|
65
|
+
return "stopped";
|
|
66
|
+
default:
|
|
67
|
+
return restarting === true ? "resident_replaced" : "lease_lapsed";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The cause a reclaim outcome records: only `closed` assigns one — a row
|
|
72
|
+
* resumed, restarted or re-hosted did not close, so a roll that resumes every
|
|
73
|
+
* row assigns nothing. */
|
|
74
|
+
export function causeOfReclaim(word: PlaneReclaimWord): PlaneEndingCause | undefined {
|
|
75
|
+
return word === "closed" ? "lease_lapsed" : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The one rendering of a cause, in the user's nouns (record 0064): every
|
|
79
|
+
* surface that says why a run ended reads this, so no note composes its own
|
|
80
|
+
* cause. */
|
|
81
|
+
export function endingCauseWords(cause: PlaneEndingCause): string {
|
|
82
|
+
switch (cause) {
|
|
83
|
+
case "completed":
|
|
84
|
+
return "it completed";
|
|
85
|
+
case "failed":
|
|
86
|
+
return "it failed";
|
|
87
|
+
case "stopped":
|
|
88
|
+
return "it was stopped";
|
|
89
|
+
case "withdrawn":
|
|
90
|
+
return "its wait was withdrawn";
|
|
91
|
+
case "refused":
|
|
92
|
+
return "its admission was refused";
|
|
93
|
+
case "lease_lapsed":
|
|
94
|
+
return "its lease lapsed with no heartbeat";
|
|
95
|
+
case "resident_replaced":
|
|
96
|
+
return "the resident container running it was replaced";
|
|
97
|
+
case "runner_gone":
|
|
98
|
+
return "the runner instance driving it is gone";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
9
101
|
|
|
10
102
|
/** The three stages an ask is judged at (record 0064): the bot's admission
|
|
11
103
|
* door, the plan runner's seed door, the resident's seat. this unit decides the
|
|
@@ -28,7 +120,12 @@ export type PlaneCondition =
|
|
|
28
120
|
* side. Each is flipped by the resident's own level report, forwarded by
|
|
29
121
|
* the bot to `POST /plane/level`, never polled. */
|
|
30
122
|
| { kind: "seat"; resident: string; met: boolean }
|
|
31
|
-
| { kind: "memory"; resident: string; met: boolean }
|
|
123
|
+
| { kind: "memory"; resident: string; met: boolean }
|
|
124
|
+
/** The provider condition (record 0064, "The queue"): the model proxy's last
|
|
125
|
+
* report for the provider is `up`. A parked run (a turn the proxy could not
|
|
126
|
+
* complete after its retry) waits on it; the provider's next relayed
|
|
127
|
+
* success, for any run, flips it. */
|
|
128
|
+
| { kind: "provider_up"; provider: string; met: boolean };
|
|
32
129
|
|
|
33
130
|
/** One resident level as the plane stores it (`plane_levels`): the side of
|
|
34
131
|
* the line the resident last reported, stamped with the report time and the
|
|
@@ -36,7 +133,9 @@ export type PlaneCondition =
|
|
|
36
133
|
* without a report — is `unknown` (`residentSideOf`), never assumed below. */
|
|
37
134
|
export interface PlaneLevelRow {
|
|
38
135
|
resident: string;
|
|
39
|
-
|
|
136
|
+
/** `provider` rows carry the model proxy's level for a provider (record
|
|
137
|
+
* 0064): `below` is `up` (the condition met), `above` is `down`. */
|
|
138
|
+
name: "seat" | "memory" | "provider";
|
|
40
139
|
side: "below" | "above";
|
|
41
140
|
reportedAt: number;
|
|
42
141
|
generation: string;
|
|
@@ -48,7 +147,7 @@ export interface PlaneLevelRow {
|
|
|
48
147
|
export function residentSideOf(
|
|
49
148
|
levels: PlaneLevelRow[],
|
|
50
149
|
resident: string,
|
|
51
|
-
name: "seat" | "memory",
|
|
150
|
+
name: "seat" | "memory" | "provider",
|
|
52
151
|
generation?: string,
|
|
53
152
|
): "below" | "above" | "unknown" {
|
|
54
153
|
const row = levels.find((l) => l.resident === resident && l.name === name);
|
|
@@ -61,7 +160,12 @@ export function residentSideOf(
|
|
|
61
160
|
* the ledger claim that promotes it (the `plane_reservations` row). A second
|
|
62
161
|
* ask meanwhile sees the thread taken and queues. The seal deletes it. */
|
|
63
162
|
export interface PlaneReservation {
|
|
64
|
-
|
|
163
|
+
/** `thread`: an admitted ask's hold. `steer`: a checkpoint steer's dedupe
|
|
164
|
+
* row, key `<runId>#<round>#<cause>` — the fixed sentence lands at most
|
|
165
|
+
* once per run per round per cause and once per round in all (record 0064,
|
|
166
|
+
* "The backpressure contract"). `park`: a run whose turn the proxy could
|
|
167
|
+
* not complete, key `<provider>#<runId>`, waiting on `provider_up`. */
|
|
168
|
+
kind: "thread" | "steer" | "park";
|
|
65
169
|
key: string;
|
|
66
170
|
runId: string;
|
|
67
171
|
at: number;
|
|
@@ -130,10 +234,62 @@ export interface PlaneAskEvent {
|
|
|
130
234
|
* nowhere, and the drain never refuses a run it waits for. */
|
|
131
235
|
restartOf?: boolean;
|
|
132
236
|
}
|
|
237
|
+
/** The facts one heartbeat carries (record 0064, "The backpressure contract"):
|
|
238
|
+
* the round index, the in-flight call with its declared bound, the last
|
|
239
|
+
* event's time and the newest pushed head. The bot assembles them from what
|
|
240
|
+
* its write-through already sees; every stamp is one it was given, never a
|
|
241
|
+
* clock it read. */
|
|
242
|
+
export interface HeartbeatFacts {
|
|
243
|
+
/** The round index: the run's step counter — a steer lands at most once per round. */
|
|
244
|
+
round: number;
|
|
245
|
+
/** Whether this is a coding (write-preset) run: only those are steered. */
|
|
246
|
+
coding: boolean;
|
|
247
|
+
/** The run's start — the no-push clock's floor before any head is pushed. */
|
|
248
|
+
startedAt: number;
|
|
249
|
+
/** The call in flight, its declared bound (a bash timeout) when it stated one,
|
|
250
|
+
* and when the run's stream last moved as it went out. */
|
|
251
|
+
inFlight?: { callId: string; tool: string; sinceAt: number; boundMs?: number };
|
|
252
|
+
/** The last event's time on the run's stream. */
|
|
253
|
+
lastEventAt?: number;
|
|
254
|
+
/** The newest pushed head the stream carried (`pushed_head`, with its `clean` fact). */
|
|
255
|
+
pushedHead?: { ref: string; sha: string; at: number; clean?: boolean };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The checkpoint steer's one fixed sentence (record 0064): the same words for
|
|
259
|
+
* every cause, so a person and a child read one instruction, never a variant. */
|
|
260
|
+
export const CHECKPOINT_STEER_SENTENCE =
|
|
261
|
+
"finish the step you are on, push a checkpoint and end the round; start no new command; the resident takes your push";
|
|
262
|
+
|
|
263
|
+
/** The steer that re-issues a held turn once its provider reports up (record 0064). */
|
|
264
|
+
export function reissueSteerSentence(provider: string): string {
|
|
265
|
+
return `the model provider ${provider} is answering again — re-issue the held turn and continue`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The checkpoint steer's causes: an in-flight call past its bound (or the
|
|
269
|
+
* no-bound line) and a coding round with no pushed head past `noPushMinutes`. */
|
|
270
|
+
export type SteerCause = "long_call" | "no_push";
|
|
271
|
+
|
|
272
|
+
/** The `dirty_at_approval` move's one brief (record 0064, "Endings and the
|
|
273
|
+
* watches"): what the fix round on the unit's coding lane is told — the
|
|
274
|
+
* reviewed-head gate voids the approval at the new head, and re-review
|
|
275
|
+
* follows, so the brief asks only for the rebase and the push. */
|
|
276
|
+
export const REBASE_ROUND_BRIEF = "rebase onto the base and push";
|
|
277
|
+
|
|
133
278
|
export type PlaneEvent =
|
|
134
279
|
| PlaneAskEvent
|
|
135
280
|
| { kind: "sealed"; at: number; threadKey: string }
|
|
136
281
|
| { kind: "withdraw"; at: number; runId: string }
|
|
282
|
+
/** One heartbeat's facts (record 0064): judged for the checkpoint steer.
|
|
283
|
+
* `noPushMs`/`noBoundMs` override the defaults (tests, config — a later
|
|
284
|
+
* unit's `plane.noPushMinutes`). */
|
|
285
|
+
| { kind: "heartbeat"; at: number; runId: string; facts: HeartbeatFacts; noPushMs?: number; noBoundMs?: number }
|
|
286
|
+
/** A provider's level as the model proxy reported it: `up` on a relayed
|
|
287
|
+
* success, `down` on a failure past its one retry. `up` re-issues every
|
|
288
|
+
* turn held parked on the provider, once each. */
|
|
289
|
+
| { kind: "provider_level"; at: number; provider: string; level: "up" | "down" }
|
|
290
|
+
/** A run parked on its provider (record 0064): the harness holds the turn,
|
|
291
|
+
* the lease keeps counting, and the provider's next `up` steers it once. */
|
|
292
|
+
| { kind: "park"; at: number; runId: string; provider: string }
|
|
137
293
|
/** A window's open or lift (`window_open`); kind `deploy` is the pending deploy (`deploy_settled`). */
|
|
138
294
|
| { kind: "window"; at: number; window: string; phase: "opened" | "lifted" }
|
|
139
295
|
/** A resident's level report (record 0064): forwarded by the bot from the levels a
|
|
@@ -155,7 +311,43 @@ export type PlaneEvent =
|
|
|
155
311
|
* waits on a resident that has said nothing within the cadence, one
|
|
156
312
|
* `probe(resident)` effect is emitted — a silent resident is probed, never
|
|
157
313
|
* waited on forever. */
|
|
158
|
-
| { kind: "reask"; at: number; cadenceMs: number }
|
|
314
|
+
| { kind: "reask"; at: number; cadenceMs: number }
|
|
315
|
+
/** A tracked pull request's title as the bot read it (`pr_opened`; the
|
|
316
|
+
* adoption read), already judged against the title rule (`check:pr-title`)
|
|
317
|
+
* by the caller — the decider is node-free and holds no vocabulary. A
|
|
318
|
+
* failing title is the `unit_title` watch (record 0064): the move is one
|
|
319
|
+
* `retitle` effect under the same rule. */
|
|
320
|
+
| { kind: "pr_tracked"; at: number; repo: string; number: number; titleOk: boolean }
|
|
321
|
+
/** A child's seal, with the facts the `orphaned_child` watch reads (record
|
|
322
|
+
* 0064): the branch its newest `pushed_head` named, the pull request its
|
|
323
|
+
* record holds, and whether a runner instance is live over it. */
|
|
324
|
+
| {
|
|
325
|
+
kind: "child_sealed";
|
|
326
|
+
at: number;
|
|
327
|
+
runId: string;
|
|
328
|
+
runnerLive: boolean;
|
|
329
|
+
repo?: string;
|
|
330
|
+
branch?: string;
|
|
331
|
+
prNumber?: number;
|
|
332
|
+
}
|
|
333
|
+
/** An approval as the merge door's pr-check read it (record 0064): the
|
|
334
|
+
* `dirty_at_approval` watch fires on `mergeableState: dirty` at an approved
|
|
335
|
+
* head — the move is a `rebase_round` effect on the unit's coding lane. */
|
|
336
|
+
| { kind: "approval"; at: number; repo: string; number: number; headSha: string; mergeableState?: string }
|
|
337
|
+
/** The engine's status for a runner instance (record 0064): read at the
|
|
338
|
+
* bot's status report, the re-ask while a seed waits, and the deadline
|
|
339
|
+
* alarm. `runner_gone` — `errored` or `terminated` with units unfinished,
|
|
340
|
+
* or the hosting deadline passed on an instance not `waiting` — emits one
|
|
341
|
+
* `reissue` keyed by the attempt number. */
|
|
342
|
+
| {
|
|
343
|
+
kind: "runner_status";
|
|
344
|
+
at: number;
|
|
345
|
+
instanceId: string;
|
|
346
|
+
status: string;
|
|
347
|
+
unfinishedUnits: string[];
|
|
348
|
+
attempt: number;
|
|
349
|
+
deadlinePassed?: boolean;
|
|
350
|
+
};
|
|
159
351
|
|
|
160
352
|
/** The closed effect union: what the bot is asked to do, offered on its
|
|
161
353
|
* heartbeat and reclaim answers and acknowledged by id (`/plane/ack`). The
|
|
@@ -172,7 +364,24 @@ export type PlaneEffect =
|
|
|
172
364
|
/** Ask the bot to probe the resident's `/status` and forward its levels
|
|
173
365
|
* (record 0064): the id is `probe:<resident>`, so the object holds at most one
|
|
174
366
|
* open probe per resident and a duplicate offer is the same effect. */
|
|
175
|
-
| { id: string; kind: "probe"; resident: string }
|
|
367
|
+
| { id: string; kind: "probe"; resident: string }
|
|
368
|
+
/** The `unit_title` move (record 0064): retitle the pull request under the
|
|
369
|
+
* title rule — the bot re-reads the title before acting, so a person's own
|
|
370
|
+
* retitle first makes this a `skipped`. Id `retitle:<repo>#<number>`. */
|
|
371
|
+
| { id: string; kind: "retitle"; repo: string; number: number }
|
|
372
|
+
/** The `orphaned_child` move (record 0064): open the pull request from the
|
|
373
|
+
* pushed branch — the same open-or-edit the recover pr-check uses, titled
|
|
374
|
+
* by the head commit's subject when it passes the title rule. A pull
|
|
375
|
+
* request already heading the branch makes this a `skipped`. */
|
|
376
|
+
| { id: string; kind: "pr_open"; repo: string; branch: string; runId: string }
|
|
377
|
+
/** The `dirty_at_approval` move (record 0064): a fix round on the unit's
|
|
378
|
+
* coding lane briefed `REBASE_ROUND_BRIEF`; the reviewed-head gate voids
|
|
379
|
+
* the approval at the new head and re-review follows. */
|
|
380
|
+
| { id: string; kind: "rebase_round"; repo: string; number: number; headSha: string; brief: string }
|
|
381
|
+
/** The `runner_gone` move (record 0064): re-issue the plan's remaining
|
|
382
|
+
* units as the next attempt — the id carries the attempt number, so a
|
|
383
|
+
* status read twice offers the same effect once. */
|
|
384
|
+
| { id: string; kind: "reissue"; instanceId: string; attempt: number; units: string[] };
|
|
176
385
|
|
|
177
386
|
/** What the object must persist beside the returned state — the decider names
|
|
178
387
|
* the rows, the object owns the SQL, both inside one `transactionSync`. */
|
|
@@ -182,9 +391,17 @@ export type PlaneWrite =
|
|
|
182
391
|
| { table: "plane_queue"; op: "state"; runId: string; state: PlaneQueueRow["state"] }
|
|
183
392
|
| { table: "plane_effects"; op: "offer"; effect: PlaneEffect; at: number }
|
|
184
393
|
| { table: "plane_reservations"; op: "put"; row: PlaneReservation }
|
|
185
|
-
| { table: "plane_reservations"; op: "del"; key: string }
|
|
394
|
+
| { table: "plane_reservations"; op: "del"; key: string; kind?: PlaneReservation["kind"] }
|
|
186
395
|
| { table: "plane_windows"; op: "put"; window: string; at: number }
|
|
187
|
-
| { table: "plane_windows"; op: "del"; window: string }
|
|
396
|
+
| { table: "plane_windows"; op: "del"; window: string }
|
|
397
|
+
/** A steer into a live run's durable inbox (run-history item 40), written in
|
|
398
|
+
* the decider's transaction: the row's sender is `plane` (record 0057's
|
|
399
|
+
* amendment), and the run reads it at its next boundary like any follow-up. */
|
|
400
|
+
| { table: "run_inbox"; op: "push"; runId: string; message: Record<string, unknown> }
|
|
401
|
+
/** A finding (record 0064): filed when no move applies, keyed by watch and
|
|
402
|
+
* subject — the object folds it with `mergePlaneFindings`, so two findings
|
|
403
|
+
* on one subject are one row with a merged timeline. */
|
|
404
|
+
| { table: "plane_findings"; op: "put"; finding: PlaneFinding };
|
|
188
405
|
|
|
189
406
|
/** The bot's own outcome for one dispatch, posted to `POST /plane/outcome`
|
|
190
407
|
* under `plane.admission: shadow` (orchestration-plane item 8): `proceeded`, `refused:<code>` or
|
|
@@ -244,7 +461,236 @@ export function decide(state: PlaneState, event: PlaneEvent): PlaneDecision {
|
|
|
244
461
|
return onObservation(state, event);
|
|
245
462
|
case "reask":
|
|
246
463
|
return onReask(state, event);
|
|
464
|
+
case "heartbeat":
|
|
465
|
+
return onHeartbeat(state, event);
|
|
466
|
+
case "provider_level":
|
|
467
|
+
return onProviderLevel(state, event);
|
|
468
|
+
case "park":
|
|
469
|
+
return onPark(state, event);
|
|
470
|
+
case "pr_tracked":
|
|
471
|
+
return onPrTracked(state, event);
|
|
472
|
+
case "child_sealed":
|
|
473
|
+
return onChildSealed(state, event);
|
|
474
|
+
case "approval":
|
|
475
|
+
return onApproval(state, event);
|
|
476
|
+
case "runner_status":
|
|
477
|
+
return onRunnerStatus(state, event);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** One finding as the moves shape it: the watch, the subject, one event —
|
|
482
|
+
* the object folds it by `planeFindingKey` (watch and subject), so the same
|
|
483
|
+
* incident observed twice is one finding with a merged timeline. */
|
|
484
|
+
function findingOf(watch: string, subject: string, at: number, what: string): PlaneFinding {
|
|
485
|
+
return { watch, subject, timeline: [{ at, what }], firstAt: at, lastAt: at };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** `unit_title` (record 0064): a tracked pull request whose title fails the
|
|
489
|
+
* rule gets one `retitle` effect under the same rule; a passing title is a
|
|
490
|
+
* no-op. The id is the pull request's, so a re-read offers the same effect. */
|
|
491
|
+
function onPrTracked(
|
|
492
|
+
state: PlaneState,
|
|
493
|
+
event: { kind: "pr_tracked"; at: number; repo: string; number: number; titleOk: boolean },
|
|
494
|
+
): PlaneDecision {
|
|
495
|
+
if (event.titleOk) return { state, effects: [], writes: [] };
|
|
496
|
+
const effect: PlaneEffect = {
|
|
497
|
+
id: `retitle:${event.repo}#${event.number}`,
|
|
498
|
+
kind: "retitle",
|
|
499
|
+
repo: event.repo,
|
|
500
|
+
number: event.number,
|
|
501
|
+
};
|
|
502
|
+
return { state, effects: [effect], writes: [{ table: "plane_effects", op: "offer", effect, at: event.at }] };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** `orphaned_child` (record 0064): a child that ends with a pushed branch, no
|
|
506
|
+
* pull request and no live runner gets a `pr_open` effect from the branch. A
|
|
507
|
+
* seal with a pull request, a live runner or no pushed branch is a no-op; a
|
|
508
|
+
* pushed branch whose repository the seal could not name is a finding — the
|
|
509
|
+
* move needs a fact the plane lacks, so it degrades instead of guessing. */
|
|
510
|
+
function onChildSealed(
|
|
511
|
+
state: PlaneState,
|
|
512
|
+
event: {
|
|
513
|
+
kind: "child_sealed";
|
|
514
|
+
at: number;
|
|
515
|
+
runId: string;
|
|
516
|
+
runnerLive: boolean;
|
|
517
|
+
repo?: string;
|
|
518
|
+
branch?: string;
|
|
519
|
+
prNumber?: number;
|
|
520
|
+
},
|
|
521
|
+
): PlaneDecision {
|
|
522
|
+
if (event.runnerLive || event.prNumber !== undefined || event.branch === undefined)
|
|
523
|
+
return { state, effects: [], writes: [] };
|
|
524
|
+
if (event.repo === undefined) {
|
|
525
|
+
const finding = findingOf(
|
|
526
|
+
"orphaned_child",
|
|
527
|
+
event.runId,
|
|
528
|
+
event.at,
|
|
529
|
+
`the run sealed with pushed branch ${event.branch}, no pull request and no live runner, and no repository is known to open one on`,
|
|
530
|
+
);
|
|
531
|
+
return { state, effects: [], writes: [{ table: "plane_findings", op: "put", finding }] };
|
|
532
|
+
}
|
|
533
|
+
const effect: PlaneEffect = {
|
|
534
|
+
id: `pr_open:${event.repo}#${event.branch}`,
|
|
535
|
+
kind: "pr_open",
|
|
536
|
+
repo: event.repo,
|
|
537
|
+
branch: event.branch,
|
|
538
|
+
runId: event.runId,
|
|
539
|
+
};
|
|
540
|
+
return { state, effects: [effect], writes: [{ table: "plane_effects", op: "offer", effect, at: event.at }] };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** `dirty_at_approval` (record 0064): `mergeableState: dirty` on an approved
|
|
544
|
+
* head opens a fix round on the unit's coding lane briefed to rebase and
|
|
545
|
+
* push; any other mergeable state — clean, unknown, unread — is a no-op. The
|
|
546
|
+
* id carries the head, so the same dirty head offers one round. */
|
|
547
|
+
function onApproval(
|
|
548
|
+
state: PlaneState,
|
|
549
|
+
event: { kind: "approval"; at: number; repo: string; number: number; headSha: string; mergeableState?: string },
|
|
550
|
+
): PlaneDecision {
|
|
551
|
+
if (event.mergeableState !== "dirty") return { state, effects: [], writes: [] };
|
|
552
|
+
const effect: PlaneEffect = {
|
|
553
|
+
id: `rebase_round:${event.repo}#${event.number}@${event.headSha}`,
|
|
554
|
+
kind: "rebase_round",
|
|
555
|
+
repo: event.repo,
|
|
556
|
+
number: event.number,
|
|
557
|
+
headSha: event.headSha,
|
|
558
|
+
brief: REBASE_ROUND_BRIEF,
|
|
559
|
+
};
|
|
560
|
+
return { state, effects: [effect], writes: [{ table: "plane_effects", op: "offer", effect, at: event.at }] };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** `runner_gone` (record 0064): the engine reports `errored` or `terminated`
|
|
564
|
+
* with units unfinished, or the hosting deadline passed on an instance the
|
|
565
|
+
* engine does not report `waiting` — one `reissue(plan, remaining units)`
|
|
566
|
+
* keyed by the attempt number, so a status read twice is the same effect. A
|
|
567
|
+
* plan with nothing unfinished has no move — the precondition is gone. */
|
|
568
|
+
function onRunnerStatus(
|
|
569
|
+
state: PlaneState,
|
|
570
|
+
event: {
|
|
571
|
+
kind: "runner_status";
|
|
572
|
+
at: number;
|
|
573
|
+
instanceId: string;
|
|
574
|
+
status: string;
|
|
575
|
+
unfinishedUnits: string[];
|
|
576
|
+
attempt: number;
|
|
577
|
+
deadlinePassed?: boolean;
|
|
578
|
+
},
|
|
579
|
+
): PlaneDecision {
|
|
580
|
+
const ended = event.status === "errored" || event.status === "terminated";
|
|
581
|
+
const overdue = event.deadlinePassed === true && event.status !== "waiting";
|
|
582
|
+
if ((!ended && !overdue) || event.unfinishedUnits.length === 0) return { state, effects: [], writes: [] };
|
|
583
|
+
const effect: PlaneEffect = {
|
|
584
|
+
id: `reissue:${event.instanceId}#${event.attempt}`,
|
|
585
|
+
kind: "reissue",
|
|
586
|
+
instanceId: event.instanceId,
|
|
587
|
+
attempt: event.attempt,
|
|
588
|
+
units: event.unfinishedUnits,
|
|
589
|
+
};
|
|
590
|
+
return { state, effects: [effect], writes: [{ table: "plane_effects", op: "offer", effect, at: event.at }] };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The inbox row a plane steer writes: sender `plane` (record 0064), the sentence as
|
|
594
|
+
* the text, and the cause under `plane` so the run page can say why. */
|
|
595
|
+
function planeInboxMessage(text: string, at: number, plane: Record<string, unknown>): Record<string, unknown> {
|
|
596
|
+
return { text, at, userId: "plane", userName: "plane", plane };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** The checkpoint steer (record 0064, "The backpressure contract"): a stalled
|
|
600
|
+
* or push-less coding run reads one fixed sentence at its next boundary. At
|
|
601
|
+
* most one reservation per run per round per cause, and — the sentence being
|
|
602
|
+
* the same for every cause — one inbox row per run per round: a second
|
|
603
|
+
* heartbeat in the same round writes none, a new round writes one again. */
|
|
604
|
+
function onHeartbeat(
|
|
605
|
+
state: PlaneState,
|
|
606
|
+
event: { kind: "heartbeat"; at: number; runId: string; facts: HeartbeatFacts; noPushMs?: number; noBoundMs?: number },
|
|
607
|
+
): PlaneDecision {
|
|
608
|
+
const { facts } = event;
|
|
609
|
+
if (!facts.coding) return { state, effects: [], writes: [] };
|
|
610
|
+
const noPushMs = event.noPushMs ?? minutesToMs(PLANE.noPushMinutes);
|
|
611
|
+
const noBoundMs = event.noBoundMs ?? minutesToMs(PLANE.noBoundMinutes);
|
|
612
|
+
const causes: SteerCause[] = [];
|
|
613
|
+
if (facts.inFlight && event.at - facts.inFlight.sinceAt > (facts.inFlight.boundMs ?? noBoundMs))
|
|
614
|
+
causes.push("long_call");
|
|
615
|
+
if (event.at - Math.max(facts.pushedHead?.at ?? 0, facts.startedAt) > noPushMs) causes.push("no_push");
|
|
616
|
+
const roundPrefix = `${event.runId}#${facts.round}#`;
|
|
617
|
+
const seen = (cause: SteerCause) =>
|
|
618
|
+
state.reservations.some((r) => r.kind === "steer" && r.key === `${roundPrefix}${cause}`);
|
|
619
|
+
const fresh = causes.filter((c) => !seen(c));
|
|
620
|
+
if (fresh.length === 0) return { state, effects: [], writes: [] };
|
|
621
|
+
const roundSteered = state.reservations.some((r) => r.kind === "steer" && r.key.startsWith(roundPrefix));
|
|
622
|
+
const rows: PlaneReservation[] = fresh.map((cause) => ({
|
|
623
|
+
kind: "steer",
|
|
624
|
+
key: `${roundPrefix}${cause}`,
|
|
625
|
+
runId: event.runId,
|
|
626
|
+
at: event.at,
|
|
627
|
+
}));
|
|
628
|
+
const writes: PlaneWrite[] = rows.map((row) => ({ table: "plane_reservations", op: "put", row }));
|
|
629
|
+
if (!roundSteered)
|
|
630
|
+
writes.push({
|
|
631
|
+
table: "run_inbox",
|
|
632
|
+
op: "push",
|
|
633
|
+
runId: event.runId,
|
|
634
|
+
message: planeInboxMessage(CHECKPOINT_STEER_SENTENCE, event.at, {
|
|
635
|
+
steer: "checkpoint",
|
|
636
|
+
causes: fresh,
|
|
637
|
+
round: facts.round,
|
|
638
|
+
}),
|
|
639
|
+
});
|
|
640
|
+
return { state: { ...state, reservations: [...state.reservations, ...rows] }, effects: [], writes };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** A provider's level (record 0064): the row is written under name `provider`
|
|
644
|
+
* (`up` ≡ `below`, `down` ≡ `above`), and an `up` re-issues every turn held
|
|
645
|
+
* parked on the provider — one steer each, the park row deleted with it —
|
|
646
|
+
* then walks the queue for anything waiting on `provider_up`. */
|
|
647
|
+
function onProviderLevel(
|
|
648
|
+
state: PlaneState,
|
|
649
|
+
event: { kind: "provider_level"; at: number; provider: string; level: "up" | "down" },
|
|
650
|
+
): PlaneDecision {
|
|
651
|
+
const row: PlaneLevelRow = {
|
|
652
|
+
resident: event.provider,
|
|
653
|
+
name: "provider",
|
|
654
|
+
side: event.level === "up" ? "below" : "above",
|
|
655
|
+
reportedAt: event.at,
|
|
656
|
+
generation: "",
|
|
657
|
+
};
|
|
658
|
+
const levels = [...state.levels.filter((l) => !(l.resident === event.provider && l.name === "provider")), row];
|
|
659
|
+
const writes: PlaneWrite[] = [{ table: "plane_levels", op: "put", row }];
|
|
660
|
+
let next = { ...state, levels };
|
|
661
|
+
if (event.level === "down") return { state: next, effects: [], writes };
|
|
662
|
+
const parked = next.reservations.filter((r) => r.kind === "park" && r.key.startsWith(`${event.provider}#`));
|
|
663
|
+
for (const p of parked) {
|
|
664
|
+
writes.push({
|
|
665
|
+
table: "run_inbox",
|
|
666
|
+
op: "push",
|
|
667
|
+
runId: p.runId,
|
|
668
|
+
message: planeInboxMessage(reissueSteerSentence(event.provider), event.at, {
|
|
669
|
+
steer: "reissue",
|
|
670
|
+
provider: event.provider,
|
|
671
|
+
}),
|
|
672
|
+
});
|
|
673
|
+
writes.push({ table: "plane_reservations", op: "del", key: p.key, kind: "park" });
|
|
247
674
|
}
|
|
675
|
+
if (parked.length > 0) next = { ...next, reservations: next.reservations.filter((r) => !parked.includes(r)) };
|
|
676
|
+
const walked = walk(next, event.at);
|
|
677
|
+
return { ...walked, writes: [...writes, ...walked.writes] };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** A run parked on its provider: one park row — a second park of the same run
|
|
681
|
+
* on the same provider is the same wait, never a second steer later. */
|
|
682
|
+
function onPark(
|
|
683
|
+
state: PlaneState,
|
|
684
|
+
event: { kind: "park"; at: number; runId: string; provider: string },
|
|
685
|
+
): PlaneDecision {
|
|
686
|
+
const key = `${event.provider}#${event.runId}`;
|
|
687
|
+
if (state.reservations.some((r) => r.kind === "park" && r.key === key)) return { state, effects: [], writes: [] };
|
|
688
|
+
const row: PlaneReservation = { kind: "park", key, runId: event.runId, at: event.at };
|
|
689
|
+
return {
|
|
690
|
+
state: { ...state, reservations: [...state.reservations, row] },
|
|
691
|
+
effects: [],
|
|
692
|
+
writes: [{ table: "plane_reservations", op: "put", row }],
|
|
693
|
+
};
|
|
248
694
|
}
|
|
249
695
|
|
|
250
696
|
/** The `/plane/admit` answer (record 0064, "The queue"): `admitted` with the
|
|
@@ -284,7 +730,9 @@ export function waitingWords(waiting: PlaneCondition[]): string {
|
|
|
284
730
|
? `a seat on ${c.resident}`
|
|
285
731
|
: c.kind === "memory"
|
|
286
732
|
? `memory on ${c.resident}`
|
|
287
|
-
:
|
|
733
|
+
: c.kind === "provider_up"
|
|
734
|
+
? `the ${c.provider} provider`
|
|
735
|
+
: `the ${c.window} window`,
|
|
288
736
|
)
|
|
289
737
|
.join(", then ");
|
|
290
738
|
}
|
|
@@ -299,7 +747,8 @@ function unmetConditionsOf(state: PlaneState, event: PlaneAskEvent): PlaneCondit
|
|
|
299
747
|
const out: PlaneCondition[] = [];
|
|
300
748
|
if (
|
|
301
749
|
event.stage === "admission" &&
|
|
302
|
-
(state.liveThreads.includes(event.threadKey) ||
|
|
750
|
+
(state.liveThreads.includes(event.threadKey) ||
|
|
751
|
+
state.reservations.some((r) => r.kind === "thread" && r.key === event.threadKey))
|
|
303
752
|
)
|
|
304
753
|
out.push({ kind: "thread_free", threadKey: event.threadKey, met: false });
|
|
305
754
|
if (!event.restartOf)
|
|
@@ -337,6 +786,7 @@ function sameCondition(a: PlaneCondition, b: PlaneCondition): boolean {
|
|
|
337
786
|
if (a.kind === "window_open" && b.kind === "window_open") return a.window === b.window;
|
|
338
787
|
if ((a.kind === "seat" && b.kind === "seat") || (a.kind === "memory" && b.kind === "memory"))
|
|
339
788
|
return a.resident === b.resident;
|
|
789
|
+
if (a.kind === "provider_up" && b.kind === "provider_up") return a.provider === b.provider;
|
|
340
790
|
return true; // deploy_settled has one subject
|
|
341
791
|
}
|
|
342
792
|
|
|
@@ -381,7 +831,7 @@ function onAsk(state: PlaneState, event: PlaneAskEvent): PlaneDecision {
|
|
|
381
831
|
|
|
382
832
|
function onSealed(state: PlaneState, event: { kind: "sealed"; at: number; threadKey: string }): PlaneDecision {
|
|
383
833
|
const liveThreads = state.liveThreads.filter((t) => t !== event.threadKey);
|
|
384
|
-
const reservations = state.reservations.filter((r) => r.key !== event.threadKey);
|
|
834
|
+
const reservations = state.reservations.filter((r) => r.kind !== "thread" || r.key !== event.threadKey);
|
|
385
835
|
const freed = liveThreads.length !== state.liveThreads.length || reservations.length !== state.reservations.length;
|
|
386
836
|
if (!freed && !hasWaiting(state, event.threadKey)) return { state, effects: [], writes: [] };
|
|
387
837
|
const writes: PlaneWrite[] =
|
|
@@ -554,7 +1004,10 @@ function conditionsMet(state: PlaneState, row: PlaneQueueRow): boolean {
|
|
|
554
1004
|
return row.conditions.every((c) => {
|
|
555
1005
|
switch (c.kind) {
|
|
556
1006
|
case "thread_free":
|
|
557
|
-
return
|
|
1007
|
+
return (
|
|
1008
|
+
!state.liveThreads.includes(c.threadKey) &&
|
|
1009
|
+
!state.reservations.some((r) => r.kind === "thread" && r.key === c.threadKey)
|
|
1010
|
+
);
|
|
558
1011
|
case "window_open":
|
|
559
1012
|
return !state.openWindows.includes(c.window);
|
|
560
1013
|
case "deploy_settled":
|
|
@@ -565,6 +1018,9 @@ function conditionsMet(state: PlaneState, row: PlaneQueueRow): boolean {
|
|
|
565
1018
|
return residentSideOf(state.levels, c.resident, "seat") === "below";
|
|
566
1019
|
case "memory":
|
|
567
1020
|
return residentSideOf(state.levels, c.resident, "memory") === "below";
|
|
1021
|
+
// A provider condition is met only by the proxy's `up` report (stored `below`).
|
|
1022
|
+
case "provider_up":
|
|
1023
|
+
return residentSideOf(state.levels, c.provider, "provider") === "below";
|
|
568
1024
|
}
|
|
569
1025
|
});
|
|
570
1026
|
}
|