@coreplane/switchboard 1.256.0 → 1.258.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 (63) 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 +49 -4
  4. package/dist/assets/deploy/cloudflare-resident/worker.ts +52 -4
  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 +65 -2
  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 +66 -18
  13. package/dist/assets/src/core/coordinator/contract.ts +45 -5
  14. package/dist/assets/src/core/coordinator/driver.ts +6 -3
  15. package/dist/assets/src/core/delivery.ts +8 -5
  16. package/dist/assets/src/core/pipelineStanding.ts +2 -0
  17. package/dist/assets/src/core/plane/decide.ts +477 -12
  18. package/dist/assets/src/core/plane/findings.ts +120 -0
  19. package/dist/assets/src/core/runEvents.ts +54 -14
  20. package/dist/assets/src/core/runFriction.ts +16 -0
  21. package/dist/assets/src/core/runLedger/types.ts +16 -3
  22. package/dist/assets/src/core/runRecord.ts +12 -0
  23. package/dist/assets/src/core/ship/contract.ts +14 -2
  24. package/dist/assets/src/core/ship/coordinator.ts +179 -51
  25. package/dist/assets/src/core/ship/renewal.ts +23 -20
  26. package/dist/assets/src/core/trace/attrs.ts +1 -1
  27. package/dist/assets/src/core/untrusted.ts +35 -0
  28. package/dist/assets/src/execution/residentCredentials.ts +7 -4
  29. package/dist/assets/web/dist/.vite/manifest.json +97 -72
  30. package/dist/assets/web/dist/assets/{DeliveryPage-BBAyLwPq.js → DeliveryPage-DadntjSp.js} +1 -1
  31. package/dist/assets/web/dist/assets/HomePage-BEEpTnVT.js +1 -0
  32. package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-DBPMLVsm.js} +1 -1
  33. package/dist/assets/web/dist/assets/PlanePage-DLusMGQ0.js +1 -0
  34. package/dist/assets/web/dist/assets/{ResidentDetailPage-jYhEeeyu.js → ResidentDetailPage-CPnHnrxy.js} +1 -1
  35. package/dist/assets/web/dist/assets/{ResidentsIndexPage-DvFmlV4U.js → ResidentsIndexPage-C9hASwHL.js} +1 -1
  36. package/dist/assets/web/dist/assets/RunFoldRow-OPe7Z0Cz.js +1 -0
  37. package/dist/assets/web/dist/assets/{RunRoutePage-CvZ-TOT3.js → RunRoutePage-o1xzkTGZ.js} +4 -4
  38. package/dist/assets/web/dist/assets/RunsIndexPage-0YAClYQh.js +1 -0
  39. package/dist/assets/web/dist/assets/{ScheduledPage-Viim-bus.js → ScheduledPage-BIdfC0V3.js} +1 -1
  40. package/dist/assets/web/dist/assets/{SettingsPage-CYUy8McC.js → SettingsPage-CnqWAjl3.js} +1 -1
  41. package/dist/assets/web/dist/assets/SilentTurn-DGNg0sIZ.js +2 -0
  42. package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
  43. package/dist/assets/web/dist/assets/{StatusDot-Dv6UMaPy.js → StatusDot-D_D_iJS8.js} +1 -1
  44. package/dist/assets/web/dist/assets/{Tooltip-Bd-5Rypv.js → Tooltip-DVFmM7on.js} +1 -1
  45. package/dist/assets/web/dist/assets/UnitRoutePage-7fAXFoB_.js +1 -0
  46. package/dist/assets/web/dist/assets/budgets-BRLm6oID.js +1 -0
  47. package/dist/assets/web/dist/assets/{dist-Nl3uaxrP.js → dist-C1VX-_mm.js} +1 -1
  48. package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
  49. package/dist/assets/web/dist/assets/{indexRow-DborJPFp.js → indexRow-BO_GIdF6.js} +1 -1
  50. package/dist/assets/web/dist/assets/main-Cl7xHG34.js +28 -0
  51. package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
  52. package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BjlKmQsY.js} +1 -1
  53. package/dist/cli.js +3096 -1136
  54. package/package.json +1 -1
  55. package/dist/assets/web/dist/assets/HomePage-BEiStFwW.js +0 -3
  56. package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
  57. package/dist/assets/web/dist/assets/RunFoldRow-DvWzR1JQ.js +0 -1
  58. package/dist/assets/web/dist/assets/RunsIndexPage-JqVSDOok.js +0 -1
  59. package/dist/assets/web/dist/assets/UnitRoutePage-B81wEnKN.js +0 -1
  60. package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +0 -1
  61. package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
  62. package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
  63. package/dist/assets/web/dist/assets/main-DRxWlffc.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,71 @@ 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 reissue steer read back (model-proxy item 12a): what the pi harness
269
+ * releases a held turn on — the sentence above with any provider's name,
270
+ * judged whole against the inbox row's text beside its `plane` sender, so a
271
+ * parked turn is only re-driven by the plane's own words, never a person's
272
+ * follow-up that happens to mention a provider. */
273
+ export function isReissueSteerText(text: string): boolean {
274
+ return /^the model provider .+ is answering again — re-issue the held turn and continue$/.test(text.trim());
275
+ }
276
+
277
+ /** The checkpoint steer's causes: an in-flight call past its bound (or the
278
+ * no-bound line) and a coding round with no pushed head past `noPushMinutes`. */
279
+ export type SteerCause = "long_call" | "no_push";
280
+
281
+ /** The `dirty_at_approval` move's one brief (record 0064, "Endings and the
282
+ * watches"): what the fix round on the unit's coding lane is told — the
283
+ * reviewed-head gate voids the approval at the new head, and re-review
284
+ * follows, so the brief asks only for the rebase and the push. */
285
+ export const REBASE_ROUND_BRIEF = "rebase onto the base and push";
286
+
133
287
  export type PlaneEvent =
134
288
  | PlaneAskEvent
135
289
  | { kind: "sealed"; at: number; threadKey: string }
136
290
  | { kind: "withdraw"; at: number; runId: string }
291
+ /** One heartbeat's facts (record 0064): judged for the checkpoint steer.
292
+ * `noPushMs`/`noBoundMs` override the defaults (tests, config — a later
293
+ * unit's `plane.noPushMinutes`). */
294
+ | { kind: "heartbeat"; at: number; runId: string; facts: HeartbeatFacts; noPushMs?: number; noBoundMs?: number }
295
+ /** A provider's level as the model proxy reported it: `up` on a relayed
296
+ * success, `down` on a failure past its one retry. `up` re-issues every
297
+ * turn held parked on the provider, once each. */
298
+ | { kind: "provider_level"; at: number; provider: string; level: "up" | "down" }
299
+ /** A run parked on its provider (record 0064): the harness holds the turn,
300
+ * the lease keeps counting, and the provider's next `up` steers it once. */
301
+ | { kind: "park"; at: number; runId: string; provider: string }
137
302
  /** A window's open or lift (`window_open`); kind `deploy` is the pending deploy (`deploy_settled`). */
138
303
  | { kind: "window"; at: number; window: string; phase: "opened" | "lifted" }
139
304
  /** A resident's level report (record 0064): forwarded by the bot from the levels a
@@ -155,7 +320,43 @@ export type PlaneEvent =
155
320
  * waits on a resident that has said nothing within the cadence, one
156
321
  * `probe(resident)` effect is emitted — a silent resident is probed, never
157
322
  * waited on forever. */
158
- | { kind: "reask"; at: number; cadenceMs: number };
323
+ | { kind: "reask"; at: number; cadenceMs: number }
324
+ /** A tracked pull request's title as the bot read it (`pr_opened`; the
325
+ * adoption read), already judged against the title rule (`check:pr-title`)
326
+ * by the caller — the decider is node-free and holds no vocabulary. A
327
+ * failing title is the `unit_title` watch (record 0064): the move is one
328
+ * `retitle` effect under the same rule. */
329
+ | { kind: "pr_tracked"; at: number; repo: string; number: number; titleOk: boolean }
330
+ /** A child's seal, with the facts the `orphaned_child` watch reads (record
331
+ * 0064): the branch its newest `pushed_head` named, the pull request its
332
+ * record holds, and whether a runner instance is live over it. */
333
+ | {
334
+ kind: "child_sealed";
335
+ at: number;
336
+ runId: string;
337
+ runnerLive: boolean;
338
+ repo?: string;
339
+ branch?: string;
340
+ prNumber?: number;
341
+ }
342
+ /** An approval as the merge door's pr-check read it (record 0064): the
343
+ * `dirty_at_approval` watch fires on `mergeableState: dirty` at an approved
344
+ * head — the move is a `rebase_round` effect on the unit's coding lane. */
345
+ | { kind: "approval"; at: number; repo: string; number: number; headSha: string; mergeableState?: string }
346
+ /** The engine's status for a runner instance (record 0064): read at the
347
+ * bot's status report, the re-ask while a seed waits, and the deadline
348
+ * alarm. `runner_gone` — `errored` or `terminated` with units unfinished,
349
+ * or the hosting deadline passed on an instance not `waiting` — emits one
350
+ * `reissue` keyed by the attempt number. */
351
+ | {
352
+ kind: "runner_status";
353
+ at: number;
354
+ instanceId: string;
355
+ status: string;
356
+ unfinishedUnits: string[];
357
+ attempt: number;
358
+ deadlinePassed?: boolean;
359
+ };
159
360
 
160
361
  /** The closed effect union: what the bot is asked to do, offered on its
161
362
  * heartbeat and reclaim answers and acknowledged by id (`/plane/ack`). The
@@ -172,7 +373,24 @@ export type PlaneEffect =
172
373
  /** Ask the bot to probe the resident's `/status` and forward its levels
173
374
  * (record 0064): the id is `probe:<resident>`, so the object holds at most one
174
375
  * open probe per resident and a duplicate offer is the same effect. */
175
- | { id: string; kind: "probe"; resident: string };
376
+ | { id: string; kind: "probe"; resident: string }
377
+ /** The `unit_title` move (record 0064): retitle the pull request under the
378
+ * title rule — the bot re-reads the title before acting, so a person's own
379
+ * retitle first makes this a `skipped`. Id `retitle:<repo>#<number>`. */
380
+ | { id: string; kind: "retitle"; repo: string; number: number }
381
+ /** The `orphaned_child` move (record 0064): open the pull request from the
382
+ * pushed branch — the same open-or-edit the recover pr-check uses, titled
383
+ * by the head commit's subject when it passes the title rule. A pull
384
+ * request already heading the branch makes this a `skipped`. */
385
+ | { id: string; kind: "pr_open"; repo: string; branch: string; runId: string }
386
+ /** The `dirty_at_approval` move (record 0064): a fix round on the unit's
387
+ * coding lane briefed `REBASE_ROUND_BRIEF`; the reviewed-head gate voids
388
+ * the approval at the new head and re-review follows. */
389
+ | { id: string; kind: "rebase_round"; repo: string; number: number; headSha: string; brief: string }
390
+ /** The `runner_gone` move (record 0064): re-issue the plan's remaining
391
+ * units as the next attempt — the id carries the attempt number, so a
392
+ * status read twice offers the same effect once. */
393
+ | { id: string; kind: "reissue"; instanceId: string; attempt: number; units: string[] };
176
394
 
177
395
  /** What the object must persist beside the returned state — the decider names
178
396
  * the rows, the object owns the SQL, both inside one `transactionSync`. */
@@ -182,9 +400,17 @@ export type PlaneWrite =
182
400
  | { table: "plane_queue"; op: "state"; runId: string; state: PlaneQueueRow["state"] }
183
401
  | { table: "plane_effects"; op: "offer"; effect: PlaneEffect; at: number }
184
402
  | { table: "plane_reservations"; op: "put"; row: PlaneReservation }
185
- | { table: "plane_reservations"; op: "del"; key: string }
403
+ | { table: "plane_reservations"; op: "del"; key: string; kind?: PlaneReservation["kind"] }
186
404
  | { table: "plane_windows"; op: "put"; window: string; at: number }
187
- | { table: "plane_windows"; op: "del"; window: string };
405
+ | { table: "plane_windows"; op: "del"; window: string }
406
+ /** A steer into a live run's durable inbox (run-history item 40), written in
407
+ * the decider's transaction: the row's sender is `plane` (record 0057's
408
+ * amendment), and the run reads it at its next boundary like any follow-up. */
409
+ | { table: "run_inbox"; op: "push"; runId: string; message: Record<string, unknown> }
410
+ /** A finding (record 0064): filed when no move applies, keyed by watch and
411
+ * subject — the object folds it with `mergePlaneFindings`, so two findings
412
+ * on one subject are one row with a merged timeline. */
413
+ | { table: "plane_findings"; op: "put"; finding: PlaneFinding };
188
414
 
189
415
  /** The bot's own outcome for one dispatch, posted to `POST /plane/outcome`
190
416
  * under `plane.admission: shadow` (orchestration-plane item 8): `proceeded`, `refused:<code>` or
@@ -244,7 +470,236 @@ export function decide(state: PlaneState, event: PlaneEvent): PlaneDecision {
244
470
  return onObservation(state, event);
245
471
  case "reask":
246
472
  return onReask(state, event);
473
+ case "heartbeat":
474
+ return onHeartbeat(state, event);
475
+ case "provider_level":
476
+ return onProviderLevel(state, event);
477
+ case "park":
478
+ return onPark(state, event);
479
+ case "pr_tracked":
480
+ return onPrTracked(state, event);
481
+ case "child_sealed":
482
+ return onChildSealed(state, event);
483
+ case "approval":
484
+ return onApproval(state, event);
485
+ case "runner_status":
486
+ return onRunnerStatus(state, event);
487
+ }
488
+ }
489
+
490
+ /** One finding as the moves shape it: the watch, the subject, one event —
491
+ * the object folds it by `planeFindingKey` (watch and subject), so the same
492
+ * incident observed twice is one finding with a merged timeline. */
493
+ function findingOf(watch: string, subject: string, at: number, what: string): PlaneFinding {
494
+ return { watch, subject, timeline: [{ at, what }], firstAt: at, lastAt: at };
495
+ }
496
+
497
+ /** `unit_title` (record 0064): a tracked pull request whose title fails the
498
+ * rule gets one `retitle` effect under the same rule; a passing title is a
499
+ * no-op. The id is the pull request's, so a re-read offers the same effect. */
500
+ function onPrTracked(
501
+ state: PlaneState,
502
+ event: { kind: "pr_tracked"; at: number; repo: string; number: number; titleOk: boolean },
503
+ ): PlaneDecision {
504
+ if (event.titleOk) return { state, effects: [], writes: [] };
505
+ const effect: PlaneEffect = {
506
+ id: `retitle:${event.repo}#${event.number}`,
507
+ kind: "retitle",
508
+ repo: event.repo,
509
+ number: event.number,
510
+ };
511
+ return { state, effects: [effect], writes: [{ table: "plane_effects", op: "offer", effect, at: event.at }] };
512
+ }
513
+
514
+ /** `orphaned_child` (record 0064): a child that ends with a pushed branch, no
515
+ * pull request and no live runner gets a `pr_open` effect from the branch. A
516
+ * seal with a pull request, a live runner or no pushed branch is a no-op; a
517
+ * pushed branch whose repository the seal could not name is a finding — the
518
+ * move needs a fact the plane lacks, so it degrades instead of guessing. */
519
+ function onChildSealed(
520
+ state: PlaneState,
521
+ event: {
522
+ kind: "child_sealed";
523
+ at: number;
524
+ runId: string;
525
+ runnerLive: boolean;
526
+ repo?: string;
527
+ branch?: string;
528
+ prNumber?: number;
529
+ },
530
+ ): PlaneDecision {
531
+ if (event.runnerLive || event.prNumber !== undefined || event.branch === undefined)
532
+ return { state, effects: [], writes: [] };
533
+ if (event.repo === undefined) {
534
+ const finding = findingOf(
535
+ "orphaned_child",
536
+ event.runId,
537
+ event.at,
538
+ `the run sealed with pushed branch ${event.branch}, no pull request and no live runner, and no repository is known to open one on`,
539
+ );
540
+ return { state, effects: [], writes: [{ table: "plane_findings", op: "put", finding }] };
247
541
  }
542
+ const effect: PlaneEffect = {
543
+ id: `pr_open:${event.repo}#${event.branch}`,
544
+ kind: "pr_open",
545
+ repo: event.repo,
546
+ branch: event.branch,
547
+ runId: event.runId,
548
+ };
549
+ return { state, effects: [effect], writes: [{ table: "plane_effects", op: "offer", effect, at: event.at }] };
550
+ }
551
+
552
+ /** `dirty_at_approval` (record 0064): `mergeableState: dirty` on an approved
553
+ * head opens a fix round on the unit's coding lane briefed to rebase and
554
+ * push; any other mergeable state — clean, unknown, unread — is a no-op. The
555
+ * id carries the head, so the same dirty head offers one round. */
556
+ function onApproval(
557
+ state: PlaneState,
558
+ event: { kind: "approval"; at: number; repo: string; number: number; headSha: string; mergeableState?: string },
559
+ ): PlaneDecision {
560
+ if (event.mergeableState !== "dirty") return { state, effects: [], writes: [] };
561
+ const effect: PlaneEffect = {
562
+ id: `rebase_round:${event.repo}#${event.number}@${event.headSha}`,
563
+ kind: "rebase_round",
564
+ repo: event.repo,
565
+ number: event.number,
566
+ headSha: event.headSha,
567
+ brief: REBASE_ROUND_BRIEF,
568
+ };
569
+ return { state, effects: [effect], writes: [{ table: "plane_effects", op: "offer", effect, at: event.at }] };
570
+ }
571
+
572
+ /** `runner_gone` (record 0064): the engine reports `errored` or `terminated`
573
+ * with units unfinished, or the hosting deadline passed on an instance the
574
+ * engine does not report `waiting` — one `reissue(plan, remaining units)`
575
+ * keyed by the attempt number, so a status read twice is the same effect. A
576
+ * plan with nothing unfinished has no move — the precondition is gone. */
577
+ function onRunnerStatus(
578
+ state: PlaneState,
579
+ event: {
580
+ kind: "runner_status";
581
+ at: number;
582
+ instanceId: string;
583
+ status: string;
584
+ unfinishedUnits: string[];
585
+ attempt: number;
586
+ deadlinePassed?: boolean;
587
+ },
588
+ ): PlaneDecision {
589
+ const ended = event.status === "errored" || event.status === "terminated";
590
+ const overdue = event.deadlinePassed === true && event.status !== "waiting";
591
+ if ((!ended && !overdue) || event.unfinishedUnits.length === 0) return { state, effects: [], writes: [] };
592
+ const effect: PlaneEffect = {
593
+ id: `reissue:${event.instanceId}#${event.attempt}`,
594
+ kind: "reissue",
595
+ instanceId: event.instanceId,
596
+ attempt: event.attempt,
597
+ units: event.unfinishedUnits,
598
+ };
599
+ return { state, effects: [effect], writes: [{ table: "plane_effects", op: "offer", effect, at: event.at }] };
600
+ }
601
+
602
+ /** The inbox row a plane steer writes: sender `plane` (record 0064), the sentence as
603
+ * the text, and the cause under `plane` so the run page can say why. */
604
+ function planeInboxMessage(text: string, at: number, plane: Record<string, unknown>): Record<string, unknown> {
605
+ return { text, at, userId: "plane", userName: "plane", plane };
606
+ }
607
+
608
+ /** The checkpoint steer (record 0064, "The backpressure contract"): a stalled
609
+ * or push-less coding run reads one fixed sentence at its next boundary. At
610
+ * most one reservation per run per round per cause, and — the sentence being
611
+ * the same for every cause — one inbox row per run per round: a second
612
+ * heartbeat in the same round writes none, a new round writes one again. */
613
+ function onHeartbeat(
614
+ state: PlaneState,
615
+ event: { kind: "heartbeat"; at: number; runId: string; facts: HeartbeatFacts; noPushMs?: number; noBoundMs?: number },
616
+ ): PlaneDecision {
617
+ const { facts } = event;
618
+ if (!facts.coding) return { state, effects: [], writes: [] };
619
+ const noPushMs = event.noPushMs ?? minutesToMs(PLANE.noPushMinutes);
620
+ const noBoundMs = event.noBoundMs ?? minutesToMs(PLANE.noBoundMinutes);
621
+ const causes: SteerCause[] = [];
622
+ if (facts.inFlight && event.at - facts.inFlight.sinceAt > (facts.inFlight.boundMs ?? noBoundMs))
623
+ causes.push("long_call");
624
+ if (event.at - Math.max(facts.pushedHead?.at ?? 0, facts.startedAt) > noPushMs) causes.push("no_push");
625
+ const roundPrefix = `${event.runId}#${facts.round}#`;
626
+ const seen = (cause: SteerCause) =>
627
+ state.reservations.some((r) => r.kind === "steer" && r.key === `${roundPrefix}${cause}`);
628
+ const fresh = causes.filter((c) => !seen(c));
629
+ if (fresh.length === 0) return { state, effects: [], writes: [] };
630
+ const roundSteered = state.reservations.some((r) => r.kind === "steer" && r.key.startsWith(roundPrefix));
631
+ const rows: PlaneReservation[] = fresh.map((cause) => ({
632
+ kind: "steer",
633
+ key: `${roundPrefix}${cause}`,
634
+ runId: event.runId,
635
+ at: event.at,
636
+ }));
637
+ const writes: PlaneWrite[] = rows.map((row) => ({ table: "plane_reservations", op: "put", row }));
638
+ if (!roundSteered)
639
+ writes.push({
640
+ table: "run_inbox",
641
+ op: "push",
642
+ runId: event.runId,
643
+ message: planeInboxMessage(CHECKPOINT_STEER_SENTENCE, event.at, {
644
+ steer: "checkpoint",
645
+ causes: fresh,
646
+ round: facts.round,
647
+ }),
648
+ });
649
+ return { state: { ...state, reservations: [...state.reservations, ...rows] }, effects: [], writes };
650
+ }
651
+
652
+ /** A provider's level (record 0064): the row is written under name `provider`
653
+ * (`up` ≡ `below`, `down` ≡ `above`), and an `up` re-issues every turn held
654
+ * parked on the provider — one steer each, the park row deleted with it —
655
+ * then walks the queue for anything waiting on `provider_up`. */
656
+ function onProviderLevel(
657
+ state: PlaneState,
658
+ event: { kind: "provider_level"; at: number; provider: string; level: "up" | "down" },
659
+ ): PlaneDecision {
660
+ const row: PlaneLevelRow = {
661
+ resident: event.provider,
662
+ name: "provider",
663
+ side: event.level === "up" ? "below" : "above",
664
+ reportedAt: event.at,
665
+ generation: "",
666
+ };
667
+ const levels = [...state.levels.filter((l) => !(l.resident === event.provider && l.name === "provider")), row];
668
+ const writes: PlaneWrite[] = [{ table: "plane_levels", op: "put", row }];
669
+ let next = { ...state, levels };
670
+ if (event.level === "down") return { state: next, effects: [], writes };
671
+ const parked = next.reservations.filter((r) => r.kind === "park" && r.key.startsWith(`${event.provider}#`));
672
+ for (const p of parked) {
673
+ writes.push({
674
+ table: "run_inbox",
675
+ op: "push",
676
+ runId: p.runId,
677
+ message: planeInboxMessage(reissueSteerSentence(event.provider), event.at, {
678
+ steer: "reissue",
679
+ provider: event.provider,
680
+ }),
681
+ });
682
+ writes.push({ table: "plane_reservations", op: "del", key: p.key, kind: "park" });
683
+ }
684
+ if (parked.length > 0) next = { ...next, reservations: next.reservations.filter((r) => !parked.includes(r)) };
685
+ const walked = walk(next, event.at);
686
+ return { ...walked, writes: [...writes, ...walked.writes] };
687
+ }
688
+
689
+ /** A run parked on its provider: one park row — a second park of the same run
690
+ * on the same provider is the same wait, never a second steer later. */
691
+ function onPark(
692
+ state: PlaneState,
693
+ event: { kind: "park"; at: number; runId: string; provider: string },
694
+ ): PlaneDecision {
695
+ const key = `${event.provider}#${event.runId}`;
696
+ if (state.reservations.some((r) => r.kind === "park" && r.key === key)) return { state, effects: [], writes: [] };
697
+ const row: PlaneReservation = { kind: "park", key, runId: event.runId, at: event.at };
698
+ return {
699
+ state: { ...state, reservations: [...state.reservations, row] },
700
+ effects: [],
701
+ writes: [{ table: "plane_reservations", op: "put", row }],
702
+ };
248
703
  }
249
704
 
250
705
  /** The `/plane/admit` answer (record 0064, "The queue"): `admitted` with the
@@ -284,7 +739,9 @@ export function waitingWords(waiting: PlaneCondition[]): string {
284
739
  ? `a seat on ${c.resident}`
285
740
  : c.kind === "memory"
286
741
  ? `memory on ${c.resident}`
287
- : `the ${c.window} window`,
742
+ : c.kind === "provider_up"
743
+ ? `the ${c.provider} provider`
744
+ : `the ${c.window} window`,
288
745
  )
289
746
  .join(", then ");
290
747
  }
@@ -299,7 +756,8 @@ function unmetConditionsOf(state: PlaneState, event: PlaneAskEvent): PlaneCondit
299
756
  const out: PlaneCondition[] = [];
300
757
  if (
301
758
  event.stage === "admission" &&
302
- (state.liveThreads.includes(event.threadKey) || state.reservations.some((r) => r.key === event.threadKey))
759
+ (state.liveThreads.includes(event.threadKey) ||
760
+ state.reservations.some((r) => r.kind === "thread" && r.key === event.threadKey))
303
761
  )
304
762
  out.push({ kind: "thread_free", threadKey: event.threadKey, met: false });
305
763
  if (!event.restartOf)
@@ -337,6 +795,7 @@ function sameCondition(a: PlaneCondition, b: PlaneCondition): boolean {
337
795
  if (a.kind === "window_open" && b.kind === "window_open") return a.window === b.window;
338
796
  if ((a.kind === "seat" && b.kind === "seat") || (a.kind === "memory" && b.kind === "memory"))
339
797
  return a.resident === b.resident;
798
+ if (a.kind === "provider_up" && b.kind === "provider_up") return a.provider === b.provider;
340
799
  return true; // deploy_settled has one subject
341
800
  }
342
801
 
@@ -381,7 +840,7 @@ function onAsk(state: PlaneState, event: PlaneAskEvent): PlaneDecision {
381
840
 
382
841
  function onSealed(state: PlaneState, event: { kind: "sealed"; at: number; threadKey: string }): PlaneDecision {
383
842
  const liveThreads = state.liveThreads.filter((t) => t !== event.threadKey);
384
- const reservations = state.reservations.filter((r) => r.key !== event.threadKey);
843
+ const reservations = state.reservations.filter((r) => r.kind !== "thread" || r.key !== event.threadKey);
385
844
  const freed = liveThreads.length !== state.liveThreads.length || reservations.length !== state.reservations.length;
386
845
  if (!freed && !hasWaiting(state, event.threadKey)) return { state, effects: [], writes: [] };
387
846
  const writes: PlaneWrite[] =
@@ -554,7 +1013,10 @@ function conditionsMet(state: PlaneState, row: PlaneQueueRow): boolean {
554
1013
  return row.conditions.every((c) => {
555
1014
  switch (c.kind) {
556
1015
  case "thread_free":
557
- return !state.liveThreads.includes(c.threadKey) && !state.reservations.some((r) => r.key === c.threadKey);
1016
+ return (
1017
+ !state.liveThreads.includes(c.threadKey) &&
1018
+ !state.reservations.some((r) => r.kind === "thread" && r.key === c.threadKey)
1019
+ );
558
1020
  case "window_open":
559
1021
  return !state.openWindows.includes(c.window);
560
1022
  case "deploy_settled":
@@ -565,6 +1027,9 @@ function conditionsMet(state: PlaneState, row: PlaneQueueRow): boolean {
565
1027
  return residentSideOf(state.levels, c.resident, "seat") === "below";
566
1028
  case "memory":
567
1029
  return residentSideOf(state.levels, c.resident, "memory") === "below";
1030
+ // A provider condition is met only by the proxy's `up` report (stored `below`).
1031
+ case "provider_up":
1032
+ return residentSideOf(state.levels, c.provider, "provider") === "below";
568
1033
  }
569
1034
  });
570
1035
  }