@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.
Files changed (61) hide show
  1. package/dist/assets/config/config.example.yaml +14 -0
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +340 -7
  3. package/dist/assets/deploy/cloudflare-resident/drain.ts +100 -1
  4. package/dist/assets/deploy/cloudflare-resident/worker.ts +227 -34
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +5 -3
  7. package/dist/assets/project.json +17 -9
  8. package/dist/assets/source.json +3 -3
  9. package/dist/assets/src/agents/registry.ts +55 -1
  10. package/dist/assets/src/core/authz/grants.ts +12 -0
  11. package/dist/assets/src/core/authz/policy.ts +15 -0
  12. package/dist/assets/src/core/budgets.ts +51 -17
  13. package/dist/assets/src/core/coordinator/contract.ts +45 -5
  14. package/dist/assets/src/core/coordinator/driver.ts +17 -4
  15. package/dist/assets/src/core/delivery.ts +8 -5
  16. package/dist/assets/src/core/pipelineStanding.ts +57 -0
  17. package/dist/assets/src/core/plane/decide.ts +468 -12
  18. package/dist/assets/src/core/plane/findings.ts +120 -0
  19. package/dist/assets/src/core/reviewVerdict.ts +49 -0
  20. package/dist/assets/src/core/runEvents.ts +41 -14
  21. package/dist/assets/src/core/runFriction.ts +16 -0
  22. package/dist/assets/src/core/runLedger/types.ts +16 -3
  23. package/dist/assets/src/core/runRecord.ts +12 -0
  24. package/dist/assets/src/core/ship/contract.ts +34 -32
  25. package/dist/assets/src/core/ship/coordinator.ts +291 -95
  26. package/dist/assets/src/core/ship/renewal.ts +19 -17
  27. package/dist/assets/src/core/trace/attrs.ts +1 -1
  28. package/dist/assets/src/core/untrusted.ts +35 -0
  29. package/dist/assets/src/execution/residentCredentials.ts +7 -4
  30. package/dist/assets/web/dist/.vite/manifest.json +82 -57
  31. package/dist/assets/web/dist/assets/HomePage-DJqJQzrL.js +1 -0
  32. package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-fhKqKUVv.js} +1 -1
  33. package/dist/assets/web/dist/assets/PlanePage-Bood0hNO.js +1 -0
  34. package/dist/assets/web/dist/assets/{ResidentDetailPage-D_RD6wLo.js → ResidentDetailPage-DOe1HtUC.js} +1 -1
  35. package/dist/assets/web/dist/assets/{ResidentsIndexPage-BLkSuCxo.js → ResidentsIndexPage-TyCGjXM4.js} +1 -1
  36. package/dist/assets/web/dist/assets/RunFoldRow-C_4t-sit.js +1 -0
  37. package/dist/assets/web/dist/assets/RunRoutePage-BNepPMRX.js +9 -0
  38. package/dist/assets/web/dist/assets/RunsIndexPage-jZnMw1mM.js +1 -0
  39. package/dist/assets/web/dist/assets/{ScheduledPage-3aYsDf-q.js → ScheduledPage-BkNoudDP.js} +1 -1
  40. package/dist/assets/web/dist/assets/{SettingsPage-DG-p5Xy1.js → SettingsPage-COvWGXl-.js} +1 -1
  41. package/dist/assets/web/dist/assets/SilentTurn-DNAANKgN.js +2 -0
  42. package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
  43. package/dist/assets/web/dist/assets/{StatusDot-CEnGlyAL.js → StatusDot-BgmkSV0S.js} +1 -1
  44. package/dist/assets/web/dist/assets/{Tooltip-CiunVowT.js → Tooltip-DEUPuCPW.js} +1 -1
  45. package/dist/assets/web/dist/assets/UnitRoutePage-DNhdzxvY.js +1 -0
  46. package/dist/assets/web/dist/assets/{dist-luhv3YSo.js → dist-CF3jz9LM.js} +1 -1
  47. package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
  48. package/dist/assets/web/dist/assets/main-7SAujY_s.js +28 -0
  49. package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
  50. package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BRJEIh83.js} +1 -1
  51. package/dist/cli.js +2941 -1116
  52. package/package.json +1 -1
  53. package/dist/assets/web/dist/assets/HomePage-Be7jLLnU.js +0 -3
  54. package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
  55. package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +0 -1
  56. package/dist/assets/web/dist/assets/RunRoutePage-CmWYGR36.js +0 -9
  57. package/dist/assets/web/dist/assets/RunsIndexPage-DW-HHuZa.js +0 -1
  58. package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +0 -1
  59. package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
  60. package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
  61. 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
- name: "seat" | "memory";
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
- kind: "thread";
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
- : `the ${c.window} window`,
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) || state.reservations.some((r) => r.key === 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 !state.liveThreads.includes(c.threadKey) && !state.reservations.some((r) => r.key === c.threadKey);
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
  }