@coreplane/switchboard 1.254.1 → 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.
- package/dist/assets/.dockerignore +3 -0
- package/dist/assets/Dockerfile +12 -1
- package/dist/assets/config/config.example.yaml +6 -1
- package/dist/assets/deploy/cloudflare/worker.ts +39 -23
- package/dist/assets/deploy/cloudflare-memory/worker.ts +552 -5
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +5 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +13 -1
- package/dist/assets/deploy/cloudflare-resident/levels.ts +84 -0
- package/dist/assets/deploy/cloudflare-resident/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +202 -31
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -1
- package/dist/assets/deploy/cloudflare-sandbox/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +21 -4
- package/dist/assets/deploy/hooks/prepare-commit-msg +17 -0
- package/dist/assets/deploy/secrets.manifest.json +12 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +21 -0
- package/dist/assets/src/core/budgets.ts +35 -2
- package/dist/assets/src/core/coordinator/contract.ts +6 -0
- package/dist/assets/src/core/coordinator/driver.ts +49 -6
- package/dist/assets/src/core/costs.ts +39 -16
- package/dist/assets/src/core/pipelineStanding.ts +5 -0
- package/dist/assets/src/core/plane/decide.ts +389 -22
- package/dist/assets/src/core/refusal.ts +3 -0
- package/dist/assets/src/core/reviewVerdict.ts +15 -2
- package/dist/assets/src/core/runEvents.ts +31 -12
- package/dist/assets/src/core/runLedger/sessionLog.ts +128 -0
- package/dist/assets/src/core/runLedger/types.ts +3 -0
- package/dist/assets/src/core/runRecord.ts +24 -7
- package/dist/assets/src/core/ship/coordinator.ts +327 -61
- package/dist/assets/src/core/trace/workerTrace.ts +3 -0
- package/dist/assets/src/execution/sandboxErrors.ts +77 -6
- package/dist/assets/web/dist/.vite/manifest.json +59 -58
- package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +1 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-ngPsO2to.js → DeliveryPage-BBAyLwPq.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-DvxTHzPx.js → HomePage-Be7jLLnU.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-DGINv9XT.js +1 -0
- package/dist/assets/web/dist/assets/{PlanePage-DpWfiX4C.js → PlanePage-JEj-lqgz.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DG86v39Y.js → ResidentDetailPage-D_RD6wLo.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-x6p689VH.js → ResidentsIndexPage-BLkSuCxo.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-ysJBY8xQ.js → RunRoutePage-CmWYGR36.js} +4 -4
- package/dist/assets/web/dist/assets/{RunsIndexPage-CuzFchcn.js → RunsIndexPage-DW-HHuZa.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BuLmfcbG.js → ScheduledPage-3aYsDf-q.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BujWkdU_.js → SettingsPage-DG-p5Xy1.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-C8Bc0pTX.js → StatusDot-CEnGlyAL.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BWwJx27K.js → Tooltip-CiunVowT.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +1 -0
- package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +1 -0
- package/dist/assets/web/dist/assets/{dist-CpnyQGOb.js → dist-luhv3YSo.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-DborJPFp.js +1 -0
- package/dist/assets/web/dist/assets/{main-Comxmwi4.js → main-mAKx_zo9.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-IzTdD4-3.js → sseReplay-DE6wv1Ua.js} +6 -6
- package/dist/cli.js +6022 -4956
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BuKjw3nv.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-B9kjA1AT.js +0 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +0 -1
- 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
|
|
17
|
-
*
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
130
|
-
if (
|
|
131
|
-
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
return
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
|
@@ -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
|
|
1035
|
-
*
|
|
1036
|
-
* the
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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;
|