@coreplane/switchboard 1.254.0 → 1.255.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/.dockerignore +3 -0
  2. package/dist/assets/Dockerfile +12 -1
  3. package/dist/assets/config/config.example.yaml +6 -1
  4. package/dist/assets/deploy/cloudflare/worker.ts +39 -23
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +552 -5
  6. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +5 -0
  7. package/dist/assets/deploy/cloudflare-resident/Dockerfile +13 -1
  8. package/dist/assets/deploy/cloudflare-resident/levels.ts +84 -0
  9. package/dist/assets/deploy/cloudflare-resident/prepare-commit-msg +17 -0
  10. package/dist/assets/deploy/cloudflare-resident/worker.ts +202 -31
  11. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -1
  12. package/dist/assets/deploy/cloudflare-sandbox/prepare-commit-msg +17 -0
  13. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +21 -4
  14. package/dist/assets/deploy/hooks/prepare-commit-msg +17 -0
  15. package/dist/assets/deploy/secrets.manifest.json +12 -0
  16. package/dist/assets/package-lock.json +3 -3
  17. package/dist/assets/package.json +1 -1
  18. package/dist/assets/source.json +3 -3
  19. package/dist/assets/src/agents/registry.ts +21 -0
  20. package/dist/assets/src/core/budgets.ts +35 -2
  21. package/dist/assets/src/core/coordinator/contract.ts +6 -0
  22. package/dist/assets/src/core/coordinator/driver.ts +49 -6
  23. package/dist/assets/src/core/costs.ts +39 -16
  24. package/dist/assets/src/core/pipelineStanding.ts +5 -0
  25. package/dist/assets/src/core/plane/decide.ts +389 -22
  26. package/dist/assets/src/core/refusal.ts +3 -0
  27. package/dist/assets/src/core/reviewVerdict.ts +15 -2
  28. package/dist/assets/src/core/runEvents.ts +32 -13
  29. package/dist/assets/src/core/runLedger/sessionLog.ts +128 -0
  30. package/dist/assets/src/core/runLedger/types.ts +3 -0
  31. package/dist/assets/src/core/runRecord.ts +24 -7
  32. package/dist/assets/src/core/ship/coordinator.ts +327 -61
  33. package/dist/assets/src/core/trace/workerTrace.ts +3 -0
  34. package/dist/assets/src/execution/sandboxErrors.ts +77 -6
  35. package/dist/assets/web/dist/.vite/manifest.json +59 -58
  36. package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +1 -0
  37. package/dist/assets/web/dist/assets/{DeliveryPage-ngPsO2to.js → DeliveryPage-BBAyLwPq.js} +1 -1
  38. package/dist/assets/web/dist/assets/{HomePage-DvxTHzPx.js → HomePage-Be7jLLnU.js} +1 -1
  39. package/dist/assets/web/dist/assets/PendingTurnRow-DGINv9XT.js +1 -0
  40. package/dist/assets/web/dist/assets/{PlanePage-DpWfiX4C.js → PlanePage-JEj-lqgz.js} +1 -1
  41. package/dist/assets/web/dist/assets/{ResidentDetailPage-DG86v39Y.js → ResidentDetailPage-D_RD6wLo.js} +1 -1
  42. package/dist/assets/web/dist/assets/{ResidentsIndexPage-x6p689VH.js → ResidentsIndexPage-BLkSuCxo.js} +1 -1
  43. package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +1 -0
  44. package/dist/assets/web/dist/assets/{RunRoutePage-ysJBY8xQ.js → RunRoutePage-CmWYGR36.js} +4 -4
  45. package/dist/assets/web/dist/assets/{RunsIndexPage-CuzFchcn.js → RunsIndexPage-DW-HHuZa.js} +1 -1
  46. package/dist/assets/web/dist/assets/{ScheduledPage-BuLmfcbG.js → ScheduledPage-3aYsDf-q.js} +1 -1
  47. package/dist/assets/web/dist/assets/{SettingsPage-BujWkdU_.js → SettingsPage-DG-p5Xy1.js} +1 -1
  48. package/dist/assets/web/dist/assets/{StatusDot-C8Bc0pTX.js → StatusDot-CEnGlyAL.js} +1 -1
  49. package/dist/assets/web/dist/assets/{Tooltip-BWwJx27K.js → Tooltip-CiunVowT.js} +1 -1
  50. package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +1 -0
  51. package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +1 -0
  52. package/dist/assets/web/dist/assets/{dist-CpnyQGOb.js → dist-luhv3YSo.js} +1 -1
  53. package/dist/assets/web/dist/assets/indexRow-DborJPFp.js +1 -0
  54. package/dist/assets/web/dist/assets/{main-Comxmwi4.js → main-mAKx_zo9.js} +2 -2
  55. package/dist/assets/web/dist/assets/{sseReplay-IzTdD4-3.js → sseReplay-DE6wv1Ua.js} +6 -6
  56. package/dist/cli.js +4154 -3061
  57. package/package.json +1 -1
  58. package/dist/assets/web/dist/assets/CostsPage-BuKjw3nv.js +0 -1
  59. package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +0 -1
  60. package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +0 -1
  61. package/dist/assets/web/dist/assets/UnitRoutePage-B9kjA1AT.js +0 -1
  62. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +0 -1
  63. package/dist/assets/web/dist/assets/indexRow-DABQtONT.js +0 -1
@@ -13,9 +13,59 @@
13
13
  export type PlaneStage = "admission" | "runner" | "resident";
14
14
 
15
15
  /** A queue condition: what must become true before the row may run. Each is
16
- * flipped by an event the plane already sees, never polled. The union grows
17
- * one member per stage as the later units land. */
18
- export type PlaneCondition = { kind: "thread_free"; threadKey: string; met: boolean };
16
+ * flipped by an event the plane already sees, never polled. The admission
17
+ * stage's full set (record 0064, "The queue"): `thread_free` — flipped by the
18
+ * seal or closing reclaim of the thread's run; `window_open` — flipped by the
19
+ * window's lift; `deploy_settled` — flipped by the deploy runner's
20
+ * `deploy.landed` post. The union grows one member per stage as the later
21
+ * units land. */
22
+ export type PlaneCondition =
23
+ | { kind: "thread_free"; threadKey: string; met: boolean }
24
+ | { kind: "window_open"; window: string; met: boolean }
25
+ | { kind: "deploy_settled"; met: boolean }
26
+ /** The resident stage's pair (record 0064, "The queue"; the resident unit):
27
+ * `seat` — the thread/op user pool has room; `memory` — the gate's soft
28
+ * side. Each is flipped by the resident's own level report, forwarded by
29
+ * the bot to `POST /plane/level`, never polled. */
30
+ | { kind: "seat"; resident: string; met: boolean }
31
+ | { kind: "memory"; resident: string; met: boolean };
32
+
33
+ /** One resident level as the plane stores it (`plane_levels`): the side of
34
+ * the line the resident last reported, stamped with the report time and the
35
+ * resident's generation. A resident with no row — or whose generation moved
36
+ * without a report — is `unknown` (`residentSideOf`), never assumed below. */
37
+ export interface PlaneLevelRow {
38
+ resident: string;
39
+ name: "seat" | "memory";
40
+ side: "below" | "above";
41
+ reportedAt: number;
42
+ generation: string;
43
+ }
44
+
45
+ /** The side a resident's level reads for an observer that knows the current
46
+ * generation: `unknown` for a resident that never reported or whose report
47
+ * predates the generation given (record 0064; the resident unit's record 0064). */
48
+ export function residentSideOf(
49
+ levels: PlaneLevelRow[],
50
+ resident: string,
51
+ name: "seat" | "memory",
52
+ generation?: string,
53
+ ): "below" | "above" | "unknown" {
54
+ const row = levels.find((l) => l.resident === resident && l.name === name);
55
+ if (!row) return "unknown";
56
+ if (generation !== undefined && row.generation !== generation) return "unknown";
57
+ return row.side;
58
+ }
59
+
60
+ /** A reservation: an admitted ask's hold on its thread between the answer and
61
+ * the ledger claim that promotes it (the `plane_reservations` row). A second
62
+ * ask meanwhile sees the thread taken and queues. The seal deletes it. */
63
+ export interface PlaneReservation {
64
+ kind: "thread";
65
+ key: string;
66
+ runId: string;
67
+ at: number;
68
+ }
19
69
 
20
70
  /** One queued ask: the `plane_queue` row. `position` counts the waiting rows
21
71
  * ahead of it on the same conditions when it queued — the number a person is
@@ -39,12 +89,28 @@ export interface PlaneQueueRow {
39
89
  export interface PlaneState {
40
90
  queue: PlaneQueueRow[];
41
91
  liveThreads: string[];
92
+ /** Threads an admitted ask holds before its claim lands (or after, until the seal). */
93
+ reservations: PlaneReservation[];
94
+ /** Open window kinds; `deploy` is the pending-deploy window (`deploy_settled` is its absence). */
95
+ openWindows: string[];
96
+ /** The residents' last level reports (`plane_levels`), one row per resident and name. */
97
+ levels: PlaneLevelRow[];
42
98
  }
43
99
 
44
100
  export function emptyPlaneState(): PlaneState {
45
- return { queue: [], liveThreads: [] };
101
+ return { queue: [], liveThreads: [], reservations: [], openWindows: [], levels: [] };
46
102
  }
47
103
 
104
+ /** The window kind behind `deploy_settled`: opened while a deploy is pending,
105
+ * lifted by the deploy runner's `deploy.landed` post (record 0064). */
106
+ export const DEPLOY_WINDOW = "deploy";
107
+
108
+ /** The window behind the resident fleet's drain (resident-repos item 69):
109
+ * opened by the registry's `set` post, lifted by its `cleared` or — from the
110
+ * registry's one alarm at `until` — `expired` post. A `restartOf` claim
111
+ * passes it like every window: the drain never refuses a run it waits for. */
112
+ export const RESIDENT_DRAIN_WINDOW = "resident-drain";
113
+
48
114
  /** The closed event union. `ask`: may this run start now; `sealed`: a thread's
49
115
  * live run ended (the ledger's seal, the event that flips `thread_free`);
50
116
  * `withdraw`: the requester gave the wait up (`runs stop` on a queued id, the transport unit). */
@@ -56,28 +122,69 @@ export interface PlaneAskEvent {
56
122
  threadKey: string;
57
123
  stage: PlaneStage;
58
124
  request: Record<string, unknown>;
125
+ /** The resident the ask is for (stage `resident` only): its seat and memory
126
+ * levels become the ask's conditions. */
127
+ resident?: string;
128
+ /** A restart of a run the resident already holds (record 0064): it
129
+ * passes the windows and the memory line — its worktree lives there or
130
+ * nowhere, and the drain never refuses a run it waits for. */
131
+ restartOf?: boolean;
59
132
  }
60
133
  export type PlaneEvent =
61
- PlaneAskEvent | { kind: "sealed"; at: number; threadKey: string } | { kind: "withdraw"; at: number; runId: string };
134
+ | PlaneAskEvent
135
+ | { kind: "sealed"; at: number; threadKey: string }
136
+ | { kind: "withdraw"; at: number; runId: string }
137
+ /** A window's open or lift (`window_open`); kind `deploy` is the pending deploy (`deploy_settled`). */
138
+ | { kind: "window"; at: number; window: string; phase: "opened" | "lifted" }
139
+ /** A resident's level report (record 0064): forwarded by the bot from the levels a
140
+ * resident answer carried, or posted from the registry's outbox. A `below`
141
+ * side walks the queue — the event that admits a waiting resident ask. */
142
+ | {
143
+ kind: "level";
144
+ at: number;
145
+ resident: string;
146
+ name: "seat" | "memory";
147
+ side: "below" | "above";
148
+ generation: string;
149
+ }
150
+ /** A refusal-by-name the bot met at attach or exec (record 0064): an admitted run
151
+ * that meets one re-enters the queue at its old position, waiting on the
152
+ * condition the refusal names, instead of falling cold. */
153
+ | { kind: "observation"; at: number; runId: string; resident: string; refusal: string }
154
+ /** The re-ask cadence (record 0064; `plane.reaskMinutes`): while a queued run
155
+ * waits on a resident that has said nothing within the cadence, one
156
+ * `probe(resident)` effect is emitted — a silent resident is probed, never
157
+ * waited on forever. */
158
+ | { kind: "reask"; at: number; cadenceMs: number };
62
159
 
63
160
  /** The closed effect union: what the bot is asked to do, offered on its
64
161
  * heartbeat and reclaim answers and acknowledged by id (`/plane/ack`). The
65
162
  * id is derived from the run, so a duplicate offer after a roll is the same
66
163
  * effect, acknowledged once. The transport unit adds execution; this one only shapes and stores. */
67
- export type PlaneEffect = {
68
- id: string;
69
- kind: "admit";
70
- runId: string;
71
- threadKey: string;
72
- request: Record<string, unknown>;
73
- };
164
+ export type PlaneEffect =
165
+ | {
166
+ id: string;
167
+ kind: "admit";
168
+ runId: string;
169
+ threadKey: string;
170
+ request: Record<string, unknown>;
171
+ }
172
+ /** Ask the bot to probe the resident's `/status` and forward its levels
173
+ * (record 0064): the id is `probe:<resident>`, so the object holds at most one
174
+ * open probe per resident and a duplicate offer is the same effect. */
175
+ | { id: string; kind: "probe"; resident: string };
74
176
 
75
177
  /** What the object must persist beside the returned state — the decider names
76
178
  * the rows, the object owns the SQL, both inside one `transactionSync`. */
77
179
  export type PlaneWrite =
180
+ | { table: "plane_levels"; op: "put"; row: PlaneLevelRow }
78
181
  | { table: "plane_queue"; op: "put"; row: PlaneQueueRow }
79
182
  | { table: "plane_queue"; op: "state"; runId: string; state: PlaneQueueRow["state"] }
80
- | { table: "plane_effects"; op: "offer"; effect: PlaneEffect; at: number };
183
+ | { table: "plane_effects"; op: "offer"; effect: PlaneEffect; at: number }
184
+ | { table: "plane_reservations"; op: "put"; row: PlaneReservation }
185
+ | { table: "plane_reservations"; op: "del"; key: string }
186
+ | { table: "plane_windows"; op: "put"; window: string; at: number }
187
+ | { table: "plane_windows"; op: "del"; window: string };
81
188
 
82
189
  /** The bot's own outcome for one dispatch, posted to `POST /plane/outcome`
83
190
  * under `plane.admission: shadow` (orchestration-plane item 8): `proceeded`, `refused:<code>` or
@@ -95,6 +202,21 @@ export interface PlaneOutcomePost {
95
202
  * `deferred` leaves it on the next heartbeat or reclaim answer. */
96
203
  export type PlaneAckOutcome = "done" | "skipped" | "deferred";
97
204
 
205
+ /** The effect bounds (record 0064, "Where it lives"): a run holds at most this
206
+ * many open effects, the object at most the total — an offer past either is
207
+ * refused by the cap's name, never queued silently. */
208
+ export const PLANE_EFFECTS_PER_RUN_CAP = 4;
209
+ export const PLANE_EFFECTS_TOTAL_CAP = 256;
210
+
211
+ /** The named refusal an over-cap offer gets (the object counts, this judges). */
212
+ export function effectCapRefusal(counts: { total: number; forRun: number }, effect: PlaneEffect): string | undefined {
213
+ if (counts.total >= PLANE_EFFECTS_TOTAL_CAP)
214
+ return `plane_effects total cap (${PLANE_EFFECTS_TOTAL_CAP}): effect ${effect.id} refused`;
215
+ if (counts.forRun >= PLANE_EFFECTS_PER_RUN_CAP)
216
+ return `plane_effects per-run cap (${PLANE_EFFECTS_PER_RUN_CAP}): effect ${effect.id} refused`;
217
+ return undefined;
218
+ }
219
+
98
220
  export interface PlaneDecision {
99
221
  state: PlaneState;
100
222
  effects: PlaneEffect[];
@@ -114,9 +236,31 @@ export function decide(state: PlaneState, event: PlaneEvent): PlaneDecision {
114
236
  return onSealed(state, event);
115
237
  case "withdraw":
116
238
  return onWithdraw(state, event);
239
+ case "window":
240
+ return onWindow(state, event);
241
+ case "level":
242
+ return onLevel(state, event);
243
+ case "observation":
244
+ return onObservation(state, event);
245
+ case "reask":
246
+ return onReask(state, event);
117
247
  }
118
248
  }
119
249
 
250
+ /** The `/plane/admit` answer (record 0064, "The queue"): `admitted` with the
251
+ * reservation the decision wrote, or `queued` with the row's id, its position
252
+ * and the conditions it waits on. */
253
+ export type PlaneAskAnswer =
254
+ | { kind: "admitted"; reservation: string }
255
+ | { kind: "queued"; id: string; position: number; waiting: PlaneCondition[] };
256
+
257
+ export function planeAskAnswerOf(decision: PlaneDecision, runId: string): PlaneAskAnswer {
258
+ const row = decision.state.queue.find((r) => r.runId === runId);
259
+ return row && row.state === "waiting"
260
+ ? { kind: "queued", id: runId, position: row.position, waiting: row.conditions }
261
+ : { kind: "admitted", reservation: runId };
262
+ }
263
+
120
264
  /** The shadow word for an ask the decider just judged (orchestration-plane item 8): `queued` when the
121
265
  * decision holds a waiting row for the run, `proceed` when it holds none —
122
266
  * what the object logs beside the bot's own outcome. */
@@ -125,17 +269,105 @@ export function planeAskWordOf(decision: PlaneDecision, runId: string): "proceed
125
269
  return row && row.state === "waiting" ? "queued" : "proceed";
126
270
  }
127
271
 
272
+ /** The queue's waiting words (record 0064, "The queue"): what a person is
273
+ * told the row waits on — the queued reply in the thread and the queued id's
274
+ * page say the same thing, so the two surfaces cannot drift. */
275
+ export function waitingWords(waiting: PlaneCondition[]): string {
276
+ if (waiting.length === 0) return "its turn";
277
+ return waiting
278
+ .map((c) =>
279
+ c.kind === "thread_free"
280
+ ? "the thread's live run"
281
+ : c.kind === "deploy_settled"
282
+ ? "the pending deploy"
283
+ : c.kind === "seat"
284
+ ? `a seat on ${c.resident}`
285
+ : c.kind === "memory"
286
+ ? `memory on ${c.resident}`
287
+ : `the ${c.window} window`,
288
+ )
289
+ .join(", then ");
290
+ }
291
+
292
+ /** The unmet conditions an ask meets right now. The admission stage asks the
293
+ * thread and the windows; the resident stage asks the windows and the
294
+ * resident's own levels — its thread is already this run's (reserved at
295
+ * admission), so it is never a condition there. A `restartOf` ask passes the
296
+ * windows and the memory line (record 0064); an `unknown` level is not a wait — the
297
+ * bot falls cold for it (record 0064) — so only a reported `above` side queues. */
298
+ function unmetConditionsOf(state: PlaneState, event: PlaneAskEvent): PlaneCondition[] {
299
+ const out: PlaneCondition[] = [];
300
+ if (
301
+ event.stage === "admission" &&
302
+ (state.liveThreads.includes(event.threadKey) || state.reservations.some((r) => r.key === event.threadKey))
303
+ )
304
+ out.push({ kind: "thread_free", threadKey: event.threadKey, met: false });
305
+ if (!event.restartOf)
306
+ for (const w of state.openWindows)
307
+ out.push(
308
+ w === DEPLOY_WINDOW ? { kind: "deploy_settled", met: false } : { kind: "window_open", window: w, met: false },
309
+ );
310
+ if (event.stage === "resident" && event.resident !== undefined) {
311
+ if (residentSideOf(state.levels, event.resident, "seat") === "above")
312
+ out.push({ kind: "seat", resident: event.resident, met: false });
313
+ if (!event.restartOf && residentSideOf(state.levels, event.resident, "memory") === "above")
314
+ out.push({ kind: "memory", resident: event.resident, met: false });
315
+ }
316
+ return out;
317
+ }
318
+
319
+ /** The condition a refusal-by-name waits on (record 0064): the pool's is the seat,
320
+ * the gate's the memory line, the drain's its window; a replaced or
321
+ * unreachable runtime waits on the resident's next seat report (the probe
322
+ * reaches it). An unrecognized refusal maps to nothing — the observation is
323
+ * a no-op and the bot's own fallback stands. */
324
+ export function conditionOfRefusal(refusal: string, resident: string): PlaneCondition | undefined {
325
+ if (refusal.startsWith("user-pool-exhausted")) return { kind: "seat", resident, met: false };
326
+ if (refusal.startsWith("memory-pressure")) return { kind: "memory", resident, met: false };
327
+ if (refusal.startsWith("draining")) return { kind: "window_open", window: RESIDENT_DRAIN_WINDOW, met: false };
328
+ if (refusal.startsWith("runtime-unreachable") || refusal.startsWith("runtime-replaced"))
329
+ return { kind: "seat", resident, met: false };
330
+ return undefined;
331
+ }
332
+
333
+ /** Whether two conditions are the same wait: same kind, same subject. */
334
+ function sameCondition(a: PlaneCondition, b: PlaneCondition): boolean {
335
+ if (a.kind !== b.kind) return false;
336
+ if (a.kind === "thread_free" && b.kind === "thread_free") return a.threadKey === b.threadKey;
337
+ if (a.kind === "window_open" && b.kind === "window_open") return a.window === b.window;
338
+ if ((a.kind === "seat" && b.kind === "seat") || (a.kind === "memory" && b.kind === "memory"))
339
+ return a.resident === b.resident;
340
+ return true; // deploy_settled has one subject
341
+ }
342
+
128
343
  function onAsk(state: PlaneState, event: PlaneAskEvent): PlaneDecision {
129
- const threadLive = state.liveThreads.includes(event.threadKey);
130
- if (!threadLive) return { state, effects: [], writes: [] };
131
- const waitingAhead = state.queue.filter((r) => r.state === "waiting" && r.threadKey === event.threadKey).length;
344
+ const conditions = unmetConditionsOf(state, event);
345
+ if (conditions.length === 0) {
346
+ // The resident stage's ask holds nothing new on admission: its thread was
347
+ // reserved at the admission stage under this same run, so a reservation
348
+ // here would only shadow it.
349
+ if (event.stage === "resident") return { state, effects: [], writes: [] };
350
+ // Admitted: the thread is reserved in the same transaction (record 0064,
351
+ // "The queue") so a second ask a moment later queues; the ledger claim
352
+ // promotes the reservation and the seal deletes it.
353
+ const reservation: PlaneReservation = { kind: "thread", key: event.threadKey, runId: event.runId, at: event.at };
354
+ return {
355
+ state: { ...state, reservations: [...state.reservations, reservation] },
356
+ effects: [],
357
+ writes: [{ table: "plane_reservations", op: "put", row: reservation }],
358
+ };
359
+ }
360
+ // Position (record 0064): the rank among queued runs sharing an unmet condition.
361
+ const waitingAhead = state.queue.filter(
362
+ (r) => r.state === "waiting" && r.conditions.some((c) => conditions.some((n) => sameCondition(c, n))),
363
+ ).length;
132
364
  const row: PlaneQueueRow = {
133
365
  runId: event.runId,
134
366
  requester: event.requester,
135
367
  threadKey: event.threadKey,
136
368
  stage: event.stage,
137
369
  request: event.request,
138
- conditions: [{ kind: "thread_free", threadKey: event.threadKey, met: false }],
370
+ conditions,
139
371
  position: waitingAhead + 1,
140
372
  queuedAt: event.at,
141
373
  state: "waiting",
@@ -149,9 +381,124 @@ function onAsk(state: PlaneState, event: PlaneAskEvent): PlaneDecision {
149
381
 
150
382
  function onSealed(state: PlaneState, event: { kind: "sealed"; at: number; threadKey: string }): PlaneDecision {
151
383
  const liveThreads = state.liveThreads.filter((t) => t !== event.threadKey);
152
- if (liveThreads.length === state.liveThreads.length && !hasWaiting(state, event.threadKey))
153
- return { state, effects: [], writes: [] };
154
- return walk({ ...state, liveThreads }, event.at);
384
+ const reservations = state.reservations.filter((r) => r.key !== event.threadKey);
385
+ const freed = liveThreads.length !== state.liveThreads.length || reservations.length !== state.reservations.length;
386
+ if (!freed && !hasWaiting(state, event.threadKey)) return { state, effects: [], writes: [] };
387
+ const writes: PlaneWrite[] =
388
+ reservations.length !== state.reservations.length
389
+ ? [{ table: "plane_reservations", op: "del", key: event.threadKey }]
390
+ : [];
391
+ const walked = walk({ ...state, liveThreads, reservations }, event.at);
392
+ return { ...walked, writes: [...writes, ...walked.writes] };
393
+ }
394
+
395
+ function onWindow(
396
+ state: PlaneState,
397
+ event: { kind: "window"; at: number; window: string; phase: string },
398
+ ): PlaneDecision {
399
+ if (event.phase === "opened") {
400
+ if (state.openWindows.includes(event.window)) return { state, effects: [], writes: [] };
401
+ return {
402
+ state: { ...state, openWindows: [...state.openWindows, event.window] },
403
+ effects: [],
404
+ writes: [{ table: "plane_windows", op: "put", window: event.window, at: event.at }],
405
+ };
406
+ }
407
+ if (!state.openWindows.includes(event.window)) return { state, effects: [], writes: [] };
408
+ const next = { ...state, openWindows: state.openWindows.filter((w) => w !== event.window) };
409
+ const walked = walk(next, event.at);
410
+ return { ...walked, writes: [{ table: "plane_windows", op: "del", window: event.window }, ...walked.writes] };
411
+ }
412
+
413
+ /** A level report (record 0064): the row is upserted, and a `below` side walks the
414
+ * queue — the admitting event for every resident condition. An unchanged
415
+ * side is still written (the report time and generation move), but only a
416
+ * crossing to `below` can admit, and the walk judges that. */
417
+ function onLevel(
418
+ state: PlaneState,
419
+ event: {
420
+ kind: "level";
421
+ at: number;
422
+ resident: string;
423
+ name: "seat" | "memory";
424
+ side: "below" | "above";
425
+ generation: string;
426
+ },
427
+ ): PlaneDecision {
428
+ const row: PlaneLevelRow = {
429
+ resident: event.resident,
430
+ name: event.name,
431
+ side: event.side,
432
+ reportedAt: event.at,
433
+ generation: event.generation,
434
+ };
435
+ const levels = [...state.levels.filter((l) => !(l.resident === event.resident && l.name === event.name)), row];
436
+ const next = { ...state, levels };
437
+ const write: PlaneWrite = { table: "plane_levels", op: "put", row };
438
+ if (event.side === "above") return { state: next, effects: [], writes: [write] };
439
+ const walked = walk(next, event.at);
440
+ return { ...walked, writes: [write, ...walked.writes] };
441
+ }
442
+
443
+ /** A refusal-by-name met by an admitted run (record 0064): its queue row re-enters
444
+ * `waiting` at its old position on the refusal's condition, and a seat or
445
+ * memory refusal is itself evidence of the side — the level row is written
446
+ * `above` so the walk does not re-admit the run into the same refusal. A run
447
+ * the queue never held, or a refusal with no condition, is a no-op. */
448
+ function onObservation(
449
+ state: PlaneState,
450
+ event: { kind: "observation"; at: number; runId: string; resident: string; refusal: string },
451
+ ): PlaneDecision {
452
+ const row = state.queue.find((r) => r.runId === event.runId && r.state === "admitted");
453
+ const condition = conditionOfRefusal(event.refusal, event.resident);
454
+ if (!row || !condition) return { state, effects: [], writes: [] };
455
+ const reentered: PlaneQueueRow = { ...row, state: "waiting", conditions: [condition] };
456
+ const writes: PlaneWrite[] = [{ table: "plane_queue", op: "put", row: reentered }];
457
+ let levels = state.levels;
458
+ if (condition.kind === "seat" || condition.kind === "memory") {
459
+ // The refusal names no generation, so the row keeps the resident's last
460
+ // known one (any name) — a refusal is evidence about the resident as it
461
+ // reports now, not about an older build.
462
+ const prior =
463
+ state.levels.find((l) => l.resident === event.resident && l.name === condition.kind) ??
464
+ state.levels.find((l) => l.resident === event.resident);
465
+ const level: PlaneLevelRow = {
466
+ resident: event.resident,
467
+ name: condition.kind,
468
+ side: "above",
469
+ reportedAt: event.at,
470
+ generation: prior?.generation ?? "",
471
+ };
472
+ levels = [...state.levels.filter((l) => !(l.resident === event.resident && l.name === condition.kind)), level];
473
+ writes.push({ table: "plane_levels", op: "put", row: level });
474
+ }
475
+ return {
476
+ state: { ...state, queue: state.queue.map((r) => (r === row ? reentered : r)), levels },
477
+ effects: [],
478
+ writes,
479
+ };
480
+ }
481
+
482
+ /** The re-ask cadence (record 0064): for every resident a waiting row waits on
483
+ * whose last report is older than the cadence (or that never reported), one
484
+ * `probe` effect — id `probe:<resident>`, so the object holds at most one
485
+ * open probe per resident. Nothing else moves: the probe's answer arrives as
486
+ * a level event and that walks the queue. */
487
+ function onReask(state: PlaneState, event: { kind: "reask"; at: number; cadenceMs: number }): PlaneDecision {
488
+ const waitedOn = new Set<string>();
489
+ for (const r of state.queue)
490
+ if (r.state === "waiting")
491
+ for (const c of r.conditions) if (c.kind === "seat" || c.kind === "memory") waitedOn.add(c.resident);
492
+ const effects: PlaneEffect[] = [];
493
+ const writes: PlaneWrite[] = [];
494
+ for (const resident of [...waitedOn].sort()) {
495
+ const latest = Math.max(0, ...state.levels.filter((l) => l.resident === resident).map((l) => l.reportedAt));
496
+ if (latest > event.at - event.cadenceMs) continue;
497
+ const effect: PlaneEffect = { id: `probe:${resident}`, kind: "probe", resident };
498
+ effects.push(effect);
499
+ writes.push({ table: "plane_effects", op: "offer", effect, at: event.at });
500
+ }
501
+ return { state, effects, writes };
155
502
  }
156
503
 
157
504
  function onWithdraw(state: PlaneState, event: { kind: "withdraw"; at: number; runId: string }): PlaneDecision {
@@ -187,17 +534,37 @@ function walk(state: PlaneState, at: number): PlaneDecision {
187
534
  threadKey: row.threadKey,
188
535
  request: row.request,
189
536
  };
537
+ // The admitted run reserves its thread like a fresh admission does, so the
538
+ // ledger claim under its id promotes the same row and a rival ask queues.
539
+ const reservation: PlaneReservation = { kind: "thread", key: row.threadKey, runId: row.runId, at };
190
540
  next = {
541
+ ...next,
191
542
  queue: next.queue.map((r) => (r === row ? admitted : r)),
192
- liveThreads: [...next.liveThreads, row.threadKey],
543
+ reservations: [...next.reservations, reservation],
193
544
  };
194
545
  effects.push(effect);
195
546
  writes.push({ table: "plane_queue", op: "state", runId: row.runId, state: "admitted" });
196
547
  writes.push({ table: "plane_effects", op: "offer", effect, at });
548
+ writes.push({ table: "plane_reservations", op: "put", row: reservation });
197
549
  }
198
550
  return { state: next, effects, writes };
199
551
  }
200
552
 
201
553
  function conditionsMet(state: PlaneState, row: PlaneQueueRow): boolean {
202
- return row.conditions.every((c) => !state.liveThreads.includes(c.threadKey));
554
+ return row.conditions.every((c) => {
555
+ switch (c.kind) {
556
+ case "thread_free":
557
+ return !state.liveThreads.includes(c.threadKey) && !state.reservations.some((r) => r.key === c.threadKey);
558
+ case "window_open":
559
+ return !state.openWindows.includes(c.window);
560
+ case "deploy_settled":
561
+ return !state.openWindows.includes(DEPLOY_WINDOW);
562
+ // A resident condition is met only by a reported `below` side: an
563
+ // `unknown` resident admits nothing — the probe reaches it first (record 0064).
564
+ case "seat":
565
+ return residentSideOf(state.levels, c.resident, "seat") === "below";
566
+ case "memory":
567
+ return residentSideOf(state.levels, c.resident, "memory") === "below";
568
+ }
569
+ });
203
570
  }
@@ -51,6 +51,9 @@ const CAUSE_OF = {
51
51
  coordinator_thread_live: "system",
52
52
  live_agent_allowlist: "policy",
53
53
  follow_up_refused: "request",
54
+ // the seed thread of a live pipeline runner (record 0051's owner rule):
55
+ // a reply there is refused naming the unit thread, never run beside it
56
+ pipeline_thread_owned: "request",
54
57
  elsewhere_agent_allowlist: "policy",
55
58
  elsewhere_follow_up_refused: "request",
56
59
  which_branch: "request",
@@ -108,6 +108,14 @@ export interface Finding {
108
108
  line?: number;
109
109
  /** One line naming the issue; the full explanation lives in the prose. */
110
110
  title: string;
111
+ /** The finding's remedy is a receipt only a person can produce — a replay
112
+ * that needs a provider credential no sandbox holds, a procedure a person
113
+ * runs live — so no fix round can address it. Set by the reviewer beside the
114
+ * severity through `submit_verdict`; ship's coordinator reads this flag,
115
+ * never prose, and a round whose actionable findings all carry it ends
116
+ * `held` (docs/reference/specs/agent-ship.md item 9). Anything but the
117
+ * literal `true` is dropped and the finding stands as actionable. */
118
+ humanGated?: true;
111
119
  /** Machine provenance: true only on a check finding the ship round's checks
112
120
  * step itself appended (ship/coordinator.ts `checkFinding`) — never set from
113
121
  * a reviewer's input, whatever id the reviewer chose. */
@@ -222,6 +230,9 @@ function parseFinding(
222
230
  // dropping the whole finding over a bad line would also drop the severity
223
231
  // that the approve→request_changes downgrade keys on.
224
232
  if (typeof r.line === "number" && Number.isInteger(r.line) && r.line >= 1) finding.line = r.line;
233
+ // Fail-open on the flag alone: a malformed humanGated never drops the
234
+ // finding — it stands as actionable, which is the conservative reading.
235
+ if (r.humanGated === true) finding.humanGated = true;
225
236
  return { finding };
226
237
  }
227
238
 
@@ -241,7 +252,7 @@ export function verdictLine(verdict: ReviewVerdict | undefined): string {
241
252
  * the posted body's list (bulleted below) and ship's synthesized child turns. */
242
253
  export function formatFinding(f: Finding): string {
243
254
  const location = f.line !== undefined ? `${f.file}:${f.line}` : f.file;
244
- return `[${f.severity}] ${f.id} ${location} — ${f.title}`;
255
+ return `[${f.severity}] ${f.id} ${location} — ${f.title}${f.humanGated ? " (human-gated)" : ""}`;
245
256
  }
246
257
 
247
258
  /** One compact disposition line — `id: fixed|declined[ — note]` — the coding
@@ -318,6 +329,7 @@ function verdictMarker(verdict: ReviewVerdict | undefined, target: ReviewBodyTar
318
329
  severity: f.severity,
319
330
  file: f.file,
320
331
  ...(f.line !== undefined ? { line: f.line } : {}),
332
+ ...(f.humanGated ? { humanGated: true } : {}),
321
333
  })),
322
334
  }
323
335
  : {}),
@@ -463,7 +475,8 @@ function isFindingShape(v: unknown): v is Finding {
463
475
  (FINDING_SEVERITIES as readonly string[]).includes(v.severity as string) &&
464
476
  typeof v.file === "string" &&
465
477
  typeof v.title === "string" &&
466
- (v.line === undefined || typeof v.line === "number")
478
+ (v.line === undefined || typeof v.line === "number") &&
479
+ (v.humanGated === undefined || v.humanGated === true)
467
480
  );
468
481
  }
469
482
 
@@ -362,7 +362,7 @@ void _everyKindListed;
362
362
  * the message, the thread's sticky preset, the user or the channel scope's
363
363
  * `agent`, `defaults.agent`, or the request router. The replay harness
364
364
  * (`load route`) reads it to tell a requester's own choice from a fallback. */
365
- export type AgentSource = "directive" | "sticky" | "user" | "channel" | "default" | "route";
365
+ export type AgentSource = "directive" | "sticky" | "user" | "channel" | "default" | "route" | "operator";
366
366
 
367
367
  /** How an operator asked a run to stop: `soft` — take no new steps and
368
368
  * wrap up through the normal finale; `hard` — abort the in-flight call now, no
@@ -418,6 +418,16 @@ export type ShipRoundOutcome =
418
418
  * 0055): the failures become check findings and the findings step runs as
419
419
  * for any changes-requested round. */
420
420
  | "checks_failed"
421
+ /** The round's coding child died on a provider transient with nothing
422
+ * pushed (issue 1932): the first such boundary marks the round's one
423
+ * re-run, a second the `transient` ending. */
424
+ | "transient"
425
+ /** The merge door enqueued the pull request — the base takes changes only
426
+ * through a merge queue (issue 2011): the unit waits for the queue's outcome. */
427
+ | "enqueued"
428
+ /** The merge queue removed the pull request: the removal reason becomes a
429
+ * finding of the round, like a red check, and a fix round follows. */
430
+ | "dequeued"
421
431
  | "aborted"
422
432
  | "stopped"
423
433
  /** The coding round ended at its lease with the unit unfinished and the row
@@ -1019,6 +1029,10 @@ export type RunEvent =
1019
1029
  command?: string;
1020
1030
  input?: { readonly [key: string]: RouteInputValue };
1021
1031
  receipt?: string;
1032
+ /** The structured seam's attempts ([record 0067](../../docs/decisions/0067-one-seam-for-a-structured-answer-a-violation-is-re-asked-with-the-violation-named-and-the-callers-declared-floor-holds-never-a-refusal-shown-to-the-person.md)):
1033
+ * what each answer violated, or that it was accepted, so a flaky model
1034
+ * is legible on the record as re-asks, not as silent floors. */
1035
+ attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
1022
1036
  outcome?: RouteOutcome;
1023
1037
  /** The refusal's code (src/core/refusal.ts) when `outcome` is `refused`
1024
1038
  * (record 0054): a refusal after a command was bound is a run
@@ -1031,28 +1045,33 @@ export type RunEvent =
1031
1045
  /** The operator's decision beside the routed request ([record 0057](../../docs/decisions/0057-the-operator-is-the-one-door-a-model-binds-every-chat-input-and-deterministic-code-authorizes-fences-and-executes.md);
1032
1046
  * the one-door plan's operator unit; run-history item 60): one per admitted chat
1033
1047
  * event under `routing.operator: shadow` or `on`, published beside the
1034
- * `route` event. The decision is binds, a question or a refusal; a bind's
1035
- * `line` is redacted and cut like the receipt (`ROUTE_RECEIPT_CAP`), never
1036
- * the message text; `intake` carries the intake gate's verdict when the
1037
- * gate is present; `latencyMs` and `outputTokens` feed the replay's median
1038
- * rows. Under `shadow` nothing runs from it. Additive: unknown → ignored. */
1048
+ * `route` event. The decision is binds, a question, a refusal or — the
1049
+ * structured seam's floor ([record 0067](../../docs/decisions/0067-one-seam-for-a-structured-answer-a-violation-is-re-asked-with-the-violation-named-and-the-callers-declared-floor-holds-never-a-refusal-shown-to-the-person.md)),
1050
+ * never the model's decision — `non_decision`: under `on` the dispatcher
1051
+ * falls back to the readers' route for that event, this event recorded on
1052
+ * the run that then runs; `attempts` lists what each answer violated or
1053
+ * that it was accepted. A bind's `line` is redacted and cut like the
1054
+ * receipt (`ROUTE_RECEIPT_CAP`), never the message text; `intake` carries
1055
+ * the intake gate's verdict when the gate is present; `latencyMs` and
1056
+ * `outputTokens` feed the replay's median rows. Under `shadow` nothing
1057
+ * runs from it. Additive: unknown → ignored. */
1039
1058
  | {
1040
1059
  type: "operator";
1041
1060
  mode: "shadow" | "on";
1042
- outcome: "binds" | "question" | "refusal";
1061
+ outcome: "binds" | "question" | "refusal" | "non_decision";
1043
1062
  reason: string;
1044
- binds?: ReadonlyArray<{ line: string; reason: string }>;
1063
+ /** A bind marked `confirmed` is a pending question's confirmed proposal
1064
+ * (`bindFromAnswer`): the line itself carries the task — the person's
1065
+ * message was the word "yes" — so a confirmed preset line routes its
1066
+ * own tail as the request. Additive: unknown → a fresh bind. */
1067
+ binds?: ReadonlyArray<{ line: string; reason: string; confirmed?: true }>;
1045
1068
  question?: string;
1046
1069
  /** A question's proposed line, redacted and cut like the receipt — what
1047
1070
  * the next turn's "yes" binds (`bindFromAnswer`). */
1048
1071
  proposal?: string;
1049
1072
  refusalCause?: string;
1050
1073
  refusalText?: string;
1051
- /** A refusal the seam itself produced (a non-decision answer, a wrong
1052
- * tool, a transport failure) — never the model's decision: under `on`
1053
- * the dispatcher falls back to the readers' route for that event, this
1054
- * event recorded on the run that then runs. */
1055
- fallback?: true;
1074
+ attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
1056
1075
  intake?: { verdict: string; reason: string };
1057
1076
  latencyMs?: number;
1058
1077
  outputTokens?: number;