@coreplane/switchboard 1.259.0 → 1.260.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/config/config.example.yaml +17 -24
- package/dist/assets/deploy/cloudflare/worker.ts +17 -56
- package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +6 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +178 -27
- package/dist/assets/deploy/cloudflare-resident/worker.ts +74 -52
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +6 -3
- package/dist/assets/deploy/profile.example.json +2 -1
- 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 +3 -5
- package/dist/assets/src/core/authz/policy.ts +5 -0
- package/dist/assets/src/core/budgets.ts +19 -6
- package/dist/assets/src/core/coordinator/contract.ts +93 -5
- package/dist/assets/src/core/coordinator/driver.ts +395 -128
- package/dist/assets/src/core/memory/types.ts +5 -0
- package/dist/assets/src/core/pipelineStanding.ts +13 -4
- package/dist/assets/src/core/plane/decide.ts +53 -12
- package/dist/assets/src/core/provider.ts +5 -0
- package/dist/assets/src/core/runEvents.ts +22 -15
- package/dist/assets/src/core/runLedger/types.ts +3 -3
- package/dist/assets/src/core/runRecord.ts +64 -11
- package/dist/assets/src/core/ship/coordinator.ts +202 -53
- package/dist/assets/src/core/ship/renewal.ts +23 -8
- package/dist/assets/src/core/types.ts +11 -0
- package/dist/assets/src/deploy/liveGate.ts +8 -0
- package/dist/assets/src/deploy/profile.ts +8 -0
- package/dist/assets/src/deploy/restart.ts +49 -35
- package/dist/assets/web/dist/.vite/manifest.json +67 -67
- package/dist/assets/web/dist/assets/{DeliveryPage-DadntjSp.js → DeliveryPage-62qCSLvk.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-Dk3HRBc3.js +1 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DBPMLVsm.js → PendingTurnRow-CeTGOXUy.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-DLusMGQ0.js → PlanePage-RD0M6O6W.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CPnHnrxy.js → ResidentDetailPage-Dl2KId95.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-C9hASwHL.js → ResidentsIndexPage-DBy43I1R.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CwHDL-Mo.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-o1xzkTGZ.js → RunRoutePage-Du1HqLZs.js} +4 -4
- package/dist/assets/web/dist/assets/RunsIndexPage-BZphy3Hr.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-BIdfC0V3.js → ScheduledPage-B7jh898t.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-CnqWAjl3.js → SettingsPage-3uLEsiiE.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-DGNg0sIZ.js → SilentTurn-CKxSxnQC.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-D_D_iJS8.js → StatusDot-x7vcK0iE.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DVFmM7on.js → Tooltip-wVXCWhMp.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-7fAXFoB_.js → UnitRoutePage-V5BXK7FH.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-KNNkT4PZ.js +1 -0
- package/dist/assets/web/dist/assets/{dist-C1VX-_mm.js → dist-C-XGnvM4.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-BO_GIdF6.js → indexRow-Dxg7C8s9.js} +1 -1
- package/dist/assets/web/dist/assets/{main-Cl7xHG34.js → main-DAL--hyj.js} +2 -2
- package/dist/assets/web/dist/assets/sseReplay-BqgOggyQ.js +11 -0
- package/dist/cli.js +9136 -8197
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-BEEpTnVT.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-OPe7Z0Cz.js +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-0YAClYQh.js +0 -1
- package/dist/assets/web/dist/assets/budgets-BRLm6oID.js +0 -1
- package/dist/assets/web/dist/assets/sseReplay-BjlKmQsY.js +0 -11
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* live in thread history; duplicating them defeats the token win. Every record
|
|
10
10
|
* keeps provenance (`sourceThreadKey`/`sourceRunId`) so a distilled fact can be
|
|
11
11
|
* re-grounded in the run that produced it. */
|
|
12
|
+
import type { Effort } from "../../effort.js";
|
|
12
13
|
import type { TraceOptions } from "../trace/types.js";
|
|
13
14
|
|
|
14
15
|
export interface MemoryRecord {
|
|
@@ -170,6 +171,10 @@ export interface MemoryConfig {
|
|
|
170
171
|
/** `<provider>/<model>` ref for the post-run reflection (write path) — a cheap
|
|
171
172
|
* tier. Absent → the run's own resolved model. Never hardcoded (invariant 7). */
|
|
172
173
|
model?: string;
|
|
174
|
+
/** How hard the extractor's model thinks, one of the five tiers; unset → the
|
|
175
|
+
* model's own default. Applied through the model card like a preset's
|
|
176
|
+
* effort (`turnEffort`, memory.md item 11). */
|
|
177
|
+
effort?: Effort;
|
|
173
178
|
/** The durable store: the Memory Worker (deploy/cloudflare-memory/). Absent →
|
|
174
179
|
* an in-process store that a restart loses (dev only; startup warns). */
|
|
175
180
|
worker?: {
|
|
@@ -94,6 +94,7 @@ export const ROUND_STAGE = {
|
|
|
94
94
|
request_changes: "fix",
|
|
95
95
|
no_verdict: "hold",
|
|
96
96
|
checks_failed: "fix",
|
|
97
|
+
checks_restarted: "approved",
|
|
97
98
|
transient: "hold",
|
|
98
99
|
enqueued: "approved",
|
|
99
100
|
dequeued: "fix",
|
|
@@ -110,7 +111,9 @@ export const ROUND_STAGE = {
|
|
|
110
111
|
* and holds the stage, and every other kind closes the unit as `ended` with
|
|
111
112
|
* the kind as detail. `blocked` — a unit status the driver posts to
|
|
112
113
|
* `unit-end` for a unit that never started — ends the same way. Pinned to
|
|
113
|
-
* the union: a new ending kind fails the build here.
|
|
114
|
+
* the union: a new ending kind fails the build here. `failed` — the ending
|
|
115
|
+
* the driver posts when a step threw inside the walk (cause `step_threw`,
|
|
116
|
+
* issue 2100) — closes the unit the same way. */
|
|
114
117
|
export const ENDING_STAGE = {
|
|
115
118
|
merged: "merged",
|
|
116
119
|
already_landed: "merged",
|
|
@@ -128,14 +131,17 @@ export const ENDING_STAGE = {
|
|
|
128
131
|
interrupted: "ended",
|
|
129
132
|
refused: "ended",
|
|
130
133
|
idle: "idle",
|
|
134
|
+
idle_expired: "ended",
|
|
131
135
|
blocked: "ended",
|
|
132
|
-
|
|
136
|
+
failed: "ended",
|
|
137
|
+
} as const satisfies Record<UnitEnding["kind"] | "blocked" | "failed", Stage | "hold">;
|
|
133
138
|
|
|
134
139
|
/** The user's words for a unit's ending (record 0066): what a card, a plan
|
|
135
140
|
* summary and the web unit page print instead of the internal token —
|
|
136
141
|
* `merge_ready` reads `merge-ready`, `round_cap` reads `round cap reached`,
|
|
137
142
|
* never the snake_case kind. Pinned to the union plus `blocked` (the driver's
|
|
138
|
-
* never-started status)
|
|
143
|
+
* never-started status) and `failed` (the driver's step-threw ending, issue
|
|
144
|
+
* 2100), so a new ending kind fails the build here. */
|
|
139
145
|
export const ENDING_WORDS = {
|
|
140
146
|
merged: "merged",
|
|
141
147
|
already_landed: "merged",
|
|
@@ -153,8 +159,10 @@ export const ENDING_WORDS = {
|
|
|
153
159
|
interrupted: "interrupted",
|
|
154
160
|
refused: "refused",
|
|
155
161
|
idle: "idle",
|
|
162
|
+
idle_expired: "idle expired",
|
|
156
163
|
blocked: "blocked",
|
|
157
|
-
|
|
164
|
+
failed: "failed",
|
|
165
|
+
} as const satisfies Record<UnitEnding["kind"] | "blocked" | "failed", string>;
|
|
158
166
|
|
|
159
167
|
/** The user's words for a round outcome (record 0066): the card's unit line
|
|
160
168
|
* prints these beside the round header, never `checks_failed` or another
|
|
@@ -167,6 +175,7 @@ export const ROUND_OUTCOME_WORDS = {
|
|
|
167
175
|
request_changes: "changes requested",
|
|
168
176
|
no_verdict: "no verdict",
|
|
169
177
|
checks_failed: "checks failed",
|
|
178
|
+
checks_restarted: "checks restarted",
|
|
170
179
|
transient: "retried",
|
|
171
180
|
enqueued: "queued to merge",
|
|
172
181
|
dequeued: "removed from the merge queue",
|
|
@@ -193,6 +193,11 @@ export interface PlaneQueueRow {
|
|
|
193
193
|
export interface PlaneState {
|
|
194
194
|
queue: PlaneQueueRow[];
|
|
195
195
|
liveThreads: string[];
|
|
196
|
+
/** The live runs and their message route: provider recovery only steers a
|
|
197
|
+
* run whose durable row still exists, and its row remains readable after a reclaim. */
|
|
198
|
+
liveRuns: Record<string, { channelId: string; threadKey: string }>;
|
|
199
|
+
/** The greatest durable inbox sequence per live run, read in the decision's transaction. */
|
|
200
|
+
inboxSeqs: Record<string, number>;
|
|
196
201
|
/** Threads an admitted ask holds before its claim lands (or after, until the seal). */
|
|
197
202
|
reservations: PlaneReservation[];
|
|
198
203
|
/** Open window kinds; `deploy` is the pending-deploy window (`deploy_settled` is its absence). */
|
|
@@ -202,7 +207,7 @@ export interface PlaneState {
|
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
export function emptyPlaneState(): PlaneState {
|
|
205
|
-
return { queue: [], liveThreads: [], reservations: [], openWindows: [], levels: [] };
|
|
210
|
+
return { queue: [], liveThreads: [], liveRuns: {}, inboxSeqs: {}, reservations: [], openWindows: [], levels: [] };
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
/** The window kind behind `deploy_settled`: opened while a deploy is pending,
|
|
@@ -374,6 +379,10 @@ export type PlaneEffect =
|
|
|
374
379
|
* (record 0064): the id is `probe:<resident>`, so the object holds at most one
|
|
375
380
|
* open probe per resident and a duplicate offer is the same effect. */
|
|
376
381
|
| { id: string; kind: "probe"; resident: string }
|
|
382
|
+
/** Deliver an already-durable provider-recovery steer to the owning live
|
|
383
|
+
* run. The inbox sequence is the idempotency key across push, heartbeat and
|
|
384
|
+
* reclaim; execution never writes the durable row again. */
|
|
385
|
+
| { id: string; kind: "steer"; runId: string; seq: number; message: PlaneSteerMessage }
|
|
377
386
|
/** The `unit_title` move (record 0064): retitle the pull request under the
|
|
378
387
|
* title rule — the bot re-reads the title before acting, so a person's own
|
|
379
388
|
* retitle first makes this a `skipped`. Id `retitle:<repo>#<number>`. */
|
|
@@ -599,12 +608,40 @@ function onRunnerStatus(
|
|
|
599
608
|
return { state, effects: [effect], writes: [{ table: "plane_effects", op: "offer", effect, at: event.at }] };
|
|
600
609
|
}
|
|
601
610
|
|
|
611
|
+
/** The typed provider-recovery row carried by both the durable inbox and its
|
|
612
|
+
* live-delivery effect. */
|
|
613
|
+
export interface PlaneSteerMessage {
|
|
614
|
+
[key: string]: unknown;
|
|
615
|
+
channelId: string;
|
|
616
|
+
threadKey: string;
|
|
617
|
+
text: string;
|
|
618
|
+
at: number;
|
|
619
|
+
userId: "plane";
|
|
620
|
+
userName: "plane";
|
|
621
|
+
plane: { steer: "reissue"; provider: string };
|
|
622
|
+
}
|
|
623
|
+
|
|
602
624
|
/** The inbox row a plane steer writes: sender `plane` (record 0064), the sentence as
|
|
603
625
|
* the text, and the cause under `plane` so the run page can say why. */
|
|
604
626
|
function planeInboxMessage(text: string, at: number, plane: Record<string, unknown>): Record<string, unknown> {
|
|
605
627
|
return { text, at, userId: "plane", userName: "plane", plane };
|
|
606
628
|
}
|
|
607
629
|
|
|
630
|
+
function reissueInboxMessage(
|
|
631
|
+
provider: string,
|
|
632
|
+
at: number,
|
|
633
|
+
route: { channelId: string; threadKey: string },
|
|
634
|
+
): PlaneSteerMessage {
|
|
635
|
+
return {
|
|
636
|
+
...route,
|
|
637
|
+
text: reissueSteerSentence(provider),
|
|
638
|
+
at,
|
|
639
|
+
userId: "plane",
|
|
640
|
+
userName: "plane",
|
|
641
|
+
plane: { steer: "reissue", provider },
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
608
645
|
/** The checkpoint steer (record 0064, "The backpressure contract"): a stalled
|
|
609
646
|
* or push-less coding run reads one fixed sentence at its next boundary. At
|
|
610
647
|
* most one reservation per run per round per cause, and — the sentence being
|
|
@@ -669,21 +706,25 @@ function onProviderLevel(
|
|
|
669
706
|
let next = { ...state, levels };
|
|
670
707
|
if (event.level === "down") return { state: next, effects: [], writes };
|
|
671
708
|
const parked = next.reservations.filter((r) => r.kind === "park" && r.key.startsWith(`${event.provider}#`));
|
|
709
|
+
const steers: PlaneEffect[] = [];
|
|
710
|
+
const inboxSeqs = { ...next.inboxSeqs };
|
|
672
711
|
for (const p of parked) {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
message
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
})
|
|
681
|
-
|
|
712
|
+
const route = next.liveRuns[p.runId];
|
|
713
|
+
if (route) {
|
|
714
|
+
const seq = (inboxSeqs[p.runId] ?? 0) + 1;
|
|
715
|
+
inboxSeqs[p.runId] = seq;
|
|
716
|
+
const message = reissueInboxMessage(event.provider, event.at, route);
|
|
717
|
+
const effect: PlaneEffect = { id: `steer:${p.runId}:${seq}`, kind: "steer", runId: p.runId, seq, message };
|
|
718
|
+
steers.push(effect);
|
|
719
|
+
writes.push({ table: "run_inbox", op: "push", runId: p.runId, message });
|
|
720
|
+
writes.push({ table: "plane_effects", op: "offer", effect, at: event.at });
|
|
721
|
+
}
|
|
682
722
|
writes.push({ table: "plane_reservations", op: "del", key: p.key, kind: "park" });
|
|
683
723
|
}
|
|
684
|
-
if (parked.length > 0)
|
|
724
|
+
if (parked.length > 0)
|
|
725
|
+
next = { ...next, inboxSeqs, reservations: next.reservations.filter((r) => !parked.includes(r)) };
|
|
685
726
|
const walked = walk(next, event.at);
|
|
686
|
-
return { ...walked, writes: [...writes, ...walked.writes] };
|
|
727
|
+
return { ...walked, effects: [...steers, ...walked.effects], writes: [...writes, ...walked.writes] };
|
|
687
728
|
}
|
|
688
729
|
|
|
689
730
|
/** A run parked on its provider: one park row — a second park of the same run
|
|
@@ -50,6 +50,11 @@ export interface CompletionRequest {
|
|
|
50
50
|
maxTokens: number;
|
|
51
51
|
/** model effort hint; providers apply it only where the model supports it */
|
|
52
52
|
effort?: Effort;
|
|
53
|
+
/** The wire word the model card decided for `effort` (`turnEffort`,
|
|
54
|
+
* routing-and-config item 2) — vouched or degraded by the card's levels
|
|
55
|
+
* map. Rides only beside `effort`; absent, a provider that applies effort
|
|
56
|
+
* sends the tier's own word. */
|
|
57
|
+
effortWord?: string;
|
|
53
58
|
/** Cancellation for a hard run stop: providers pass it to their HTTP
|
|
54
59
|
* call so an aborted run stops billing/streaming now. Absent → never aborts. */
|
|
55
60
|
signal?: AbortSignal;
|
|
@@ -294,6 +294,10 @@ export type RunNoteKind =
|
|
|
294
294
|
* there was nothing to push), so a re-issue starts from the partial work
|
|
295
295
|
* (docs/reference/specs/agent-ship.md item 8). */
|
|
296
296
|
| "budget_salvage"
|
|
297
|
+
/** A coordinator's coding child ended failed, stopped or interrupted with
|
|
298
|
+
* work in its tree: before teardown the run loop made a WIP commit and
|
|
299
|
+
* pushed the unit branch, or recorded why no push was possible. */
|
|
300
|
+
| "work_salvage"
|
|
297
301
|
/** A pi run's compaction failed for good — the provider refused the summary
|
|
298
302
|
* (harness-pi.md item 7) — and the run loop treated it as a checkpoint
|
|
299
303
|
* signal: the tracked changes were committed and pushed to the run's own
|
|
@@ -363,6 +367,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
363
367
|
"tool_unnamed",
|
|
364
368
|
"directory_reached",
|
|
365
369
|
"budget_salvage",
|
|
370
|
+
"work_salvage",
|
|
366
371
|
"compaction_salvage",
|
|
367
372
|
"stuck_loop",
|
|
368
373
|
"decline_cascade",
|
|
@@ -432,6 +437,9 @@ export type ShipRoundOutcome =
|
|
|
432
437
|
* 0055): the failures become check findings and the findings step runs as
|
|
433
438
|
* for any changes-requested round. */
|
|
434
439
|
| "checks_failed"
|
|
440
|
+
/** No required check appeared during the checks step's grace chunk, so the
|
|
441
|
+
* runner re-fired the pull_request event once by closing and reopening it. */
|
|
442
|
+
| "checks_restarted"
|
|
435
443
|
/** The round's coding child died on a provider transient with nothing
|
|
436
444
|
* pushed (issue 1932): the first such boundary marks the round's one
|
|
437
445
|
* re-run, a second the `transient` ending. */
|
|
@@ -1070,15 +1078,13 @@ export type RunEvent =
|
|
|
1070
1078
|
seq?: number;
|
|
1071
1079
|
at?: number;
|
|
1072
1080
|
}
|
|
1073
|
-
/** The operator's decision
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
* the run that then runs; `attempts` lists what each answer violated or
|
|
1081
|
-
* that it was accepted. A bind's `line` is redacted and cut like the
|
|
1081
|
+
/** The operator's decision ([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);
|
|
1082
|
+
* run-history item 60): one per admitted chat event under
|
|
1083
|
+
* `routing.operator: shadow` or `on`. A no-call turn is repaired once and a
|
|
1084
|
+
* second becomes a typed general bind with reason `no_decision`; another
|
|
1085
|
+
* exhausted violation may remain `non_decision` and resolves on the
|
|
1086
|
+
* configured default with no second model. `attempts` lists what each
|
|
1087
|
+
* answer violated or that it was accepted. A bind's `line` is redacted and cut like the
|
|
1082
1088
|
* receipt (`ROUTE_RECEIPT_CAP`), never the message text; `intake` carries
|
|
1083
1089
|
* the intake gate's verdict when the gate is present; `latencyMs` and
|
|
1084
1090
|
* `outputTokens` feed the replay's median rows. Under `shadow` nothing
|
|
@@ -1088,16 +1094,17 @@ export type RunEvent =
|
|
|
1088
1094
|
mode: "shadow" | "on";
|
|
1089
1095
|
outcome: "binds" | "question" | "refusal" | "non_decision";
|
|
1090
1096
|
reason: string;
|
|
1091
|
-
/**
|
|
1092
|
-
*
|
|
1093
|
-
* readers' route ran the person's own request, and the event never
|
|
1094
|
-
* re-enters the loop. Additive: unknown → ignored. */
|
|
1097
|
+
/** Historical marker from records written before the readers' router
|
|
1098
|
+
* retired. New events never emit it. Additive: unknown → ignored. */
|
|
1095
1099
|
floored?: true;
|
|
1096
1100
|
/** A bind marked `confirmed` is a pending question's confirmed proposal
|
|
1097
1101
|
* (`bindFromAnswer`): the line itself carries the task — the person's
|
|
1098
1102
|
* message was the word "yes" — so a confirmed preset line routes its
|
|
1099
|
-
* own tail as the request. Additive: unknown → a fresh bind.
|
|
1100
|
-
|
|
1103
|
+
* own tail as the request. Additive: unknown → a fresh bind. A bind's
|
|
1104
|
+
* `model` is the ref the run uses, resolved from a model the person
|
|
1105
|
+
* named in plain words (the plain-words model unit) — applied at
|
|
1106
|
+
* directive precedence. Additive: unknown → no model. */
|
|
1107
|
+
binds?: ReadonlyArray<{ line: string; reason: string; model?: string; repo?: string; confirmed?: true }>;
|
|
1101
1108
|
question?: string;
|
|
1102
1109
|
/** A question's proposed line, redacted and cut like the receipt — what
|
|
1103
1110
|
* the next turn's "yes" binds (`bindFromAnswer`). */
|
|
@@ -128,9 +128,9 @@ export interface LiveRunMeta {
|
|
|
128
128
|
export type RunState = Record<string, unknown>;
|
|
129
129
|
|
|
130
130
|
/** A hosted ship parent's hosting fact on the row's state (record 0060): the
|
|
131
|
-
* runner instance the run hosts and
|
|
132
|
-
*
|
|
133
|
-
*
|
|
131
|
+
* runner instance the run hosts and its original wall-clock estimate. Reclaim
|
|
132
|
+
* may outlive that estimate while the Workflow or one of its child rows is
|
|
133
|
+
* live; only no durable owner beyond it closes the row. */
|
|
134
134
|
export interface HostingState {
|
|
135
135
|
instanceId: string;
|
|
136
136
|
/** Epoch ms: the hand-off time plus the instance's `caps.maxMinutes` plus
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./reviewVerdict.js";
|
|
16
16
|
import { IDEMPOTENCY_KEY_PATTERN, INSTANCE_ID_PATTERN } from "./coordinator/contract.js";
|
|
17
17
|
import { isPipelineSummaryShape, type PipelineSummary } from "./pipelineStanding.js";
|
|
18
|
+
import { RESTART_CLAIM_GRACE_MS } from "./budgets.js";
|
|
18
19
|
import {
|
|
19
20
|
FRICTION_CATEGORIES,
|
|
20
21
|
type CategoryTotals,
|
|
@@ -231,6 +232,11 @@ export interface RunRecord {
|
|
|
231
232
|
* waiting for `child_resumed` instead of ending its unit on this record.
|
|
232
233
|
* Absent on every ending that is final. */
|
|
233
234
|
restarting?: true;
|
|
235
|
+
/** Epoch ms through which a `restarting` close with no visible successor is
|
|
236
|
+
* projected as running. Current writers stamp one central grace from the
|
|
237
|
+
* close; absent on legacy restarting records, which retain the old
|
|
238
|
+
* unbounded compatibility behavior, and on every final ending. */
|
|
239
|
+
restartUntil?: number;
|
|
234
240
|
/** Where the run's conversation started (item 52): `channel` — its own
|
|
235
241
|
* thread's history, as for every run a person, a schedule or a coordinator
|
|
236
242
|
* started — or `parent` — a spawned child seeded from its parent's text
|
|
@@ -283,19 +289,23 @@ export interface RunLease {
|
|
|
283
289
|
loopEndsAt: number;
|
|
284
290
|
}
|
|
285
291
|
|
|
286
|
-
/** A head a run pushed, as the record names it (item 2): the branch
|
|
292
|
+
/** A head a run pushed, as the record names it (item 2): the branch, sha,
|
|
293
|
+
* and whether the agent pushed it or the run's mechanical WIP salvage did.
|
|
294
|
+
* `by` is optional for records written before that distinction survived the
|
|
295
|
+
* event fold. */
|
|
287
296
|
export interface PushedHead {
|
|
288
297
|
ref: string;
|
|
289
298
|
sha: string;
|
|
299
|
+
by?: "push" | "salvage";
|
|
290
300
|
}
|
|
291
301
|
|
|
292
302
|
/** The heads a run's events say it pushed — one per branch, the last event's
|
|
293
303
|
* sha winning, in first-seen order — or nothing when it pushed none. */
|
|
294
304
|
export function pushedHeadsOf(events: readonly RunEvent[]): PushedHead[] | undefined {
|
|
295
|
-
const byRef = new Map<string, string>();
|
|
296
|
-
for (const e of events) if (e.type === "pushed_head") byRef.set(e.ref, e.sha);
|
|
305
|
+
const byRef = new Map<string, { sha: string; by: "push" | "salvage" }>();
|
|
306
|
+
for (const e of events) if (e.type === "pushed_head") byRef.set(e.ref, { sha: e.sha, by: e.by });
|
|
297
307
|
if (byRef.size === 0) return undefined;
|
|
298
|
-
return [...byRef].map(([ref,
|
|
308
|
+
return [...byRef].map(([ref, pushed]) => ({ ref, ...pushed }));
|
|
299
309
|
}
|
|
300
310
|
|
|
301
311
|
/** The lease a run's events say its harness started — the first `lease`
|
|
@@ -434,7 +444,7 @@ export interface RunOperatorDecision {
|
|
|
434
444
|
mode: "shadow" | "on";
|
|
435
445
|
outcome: "binds" | "question" | "refusal" | "non_decision";
|
|
436
446
|
reason: string;
|
|
437
|
-
binds?: { line: string; reason: string }[];
|
|
447
|
+
binds?: { line: string; reason: string; model?: string; repo?: string; confirmed?: true }[];
|
|
438
448
|
question?: string;
|
|
439
449
|
/** A question's proposed line — what the next turn's "yes" binds. */
|
|
440
450
|
proposal?: string;
|
|
@@ -462,7 +472,17 @@ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecisi
|
|
|
462
472
|
mode: e.mode,
|
|
463
473
|
outcome: e.outcome,
|
|
464
474
|
reason: e.reason,
|
|
465
|
-
...(e.binds
|
|
475
|
+
...(e.binds
|
|
476
|
+
? {
|
|
477
|
+
binds: e.binds.map((b) => ({
|
|
478
|
+
line: b.line,
|
|
479
|
+
reason: b.reason,
|
|
480
|
+
...(b.model !== undefined ? { model: b.model } : {}),
|
|
481
|
+
...(b.repo !== undefined ? { repo: b.repo } : {}),
|
|
482
|
+
...(b.confirmed ? { confirmed: true as const } : {}),
|
|
483
|
+
})),
|
|
484
|
+
}
|
|
485
|
+
: {}),
|
|
466
486
|
...(e.question !== undefined ? { question: e.question } : {}),
|
|
467
487
|
...(e.proposal !== undefined ? { proposal: e.proposal } : {}),
|
|
468
488
|
...(e.request !== undefined ? { request: e.request } : {}),
|
|
@@ -944,11 +964,34 @@ export function normalizeDiagnosis(d: FrictionDiagnosis): FrictionDiagnosis {
|
|
|
944
964
|
/** `normalizeDiagnosis` applied to anything carrying a `diagnosis` — a record or
|
|
945
965
|
* a listing row — on its way out of a store, and the `channelVisibility` stamp
|
|
946
966
|
* filled with `unknown` for a row written before it existed (fail-closed:
|
|
947
|
-
* `unknown` is never public).
|
|
967
|
+
* `unknown` is never public). A restart deadline is trusted only inside the
|
|
968
|
+
* central grace after its persisted close stamp; malformed or overlong
|
|
969
|
+
* values drop the restarting projection, while a legacy row with no deadline
|
|
970
|
+
* keeps its compatibility behavior. */
|
|
948
971
|
export function normalizeStored<T extends { diagnosis: FrictionDiagnosis; channelVisibility?: ChannelVisibility }>(
|
|
949
972
|
v: T,
|
|
950
973
|
): T & { channelVisibility: ChannelVisibility } {
|
|
951
|
-
|
|
974
|
+
const normalized = {
|
|
975
|
+
...v,
|
|
976
|
+
diagnosis: normalizeDiagnosis(v.diagnosis),
|
|
977
|
+
channelVisibility: v.channelVisibility ?? "unknown",
|
|
978
|
+
} as T & { channelVisibility: ChannelVisibility };
|
|
979
|
+
const restart = normalized as T & { restarting?: unknown; restartUntil?: unknown; finishedAt?: unknown };
|
|
980
|
+
if (restart.restartUntil !== undefined) {
|
|
981
|
+
const bounded =
|
|
982
|
+
restart.restarting === true &&
|
|
983
|
+
typeof restart.restartUntil === "number" &&
|
|
984
|
+
Number.isFinite(restart.restartUntil) &&
|
|
985
|
+
typeof restart.finishedAt === "number" &&
|
|
986
|
+
Number.isFinite(restart.finishedAt) &&
|
|
987
|
+
restart.restartUntil >= restart.finishedAt &&
|
|
988
|
+
restart.restartUntil <= restart.finishedAt + RESTART_CLAIM_GRACE_MS;
|
|
989
|
+
if (!bounded) {
|
|
990
|
+
delete restart.restarting;
|
|
991
|
+
delete restart.restartUntil;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return normalized;
|
|
952
995
|
}
|
|
953
996
|
|
|
954
997
|
/** Structural check on a record from outside the process (a Worker response, a
|
|
@@ -990,8 +1033,14 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
990
1033
|
Array.isArray(r.pushed) &&
|
|
991
1034
|
r.pushed.every((h) => {
|
|
992
1035
|
if (typeof h !== "object" || h === null) return false;
|
|
993
|
-
const { ref, sha } = h as Record<string, unknown>;
|
|
994
|
-
return
|
|
1036
|
+
const { ref, sha, by } = h as Record<string, unknown>;
|
|
1037
|
+
return (
|
|
1038
|
+
typeof ref === "string" &&
|
|
1039
|
+
ref.length > 0 &&
|
|
1040
|
+
typeof sha === "string" &&
|
|
1041
|
+
/^[0-9a-f]{7,40}$/.test(sha) &&
|
|
1042
|
+
(by === undefined || by === "push" || by === "salvage")
|
|
1043
|
+
);
|
|
995
1044
|
})
|
|
996
1045
|
)
|
|
997
1046
|
)
|
|
@@ -1027,8 +1076,12 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
1027
1076
|
// marker, like `provisional`, is the literal `true` or absent.
|
|
1028
1077
|
if (r.pipeline !== undefined && !isPipelineSummaryShape(r.pipeline)) return false;
|
|
1029
1078
|
if (r.hosted !== undefined && r.hosted !== true) return false;
|
|
1030
|
-
// A restarting close carries the literal `true`
|
|
1079
|
+
// A restarting close carries the literal `true` and, on current writers,
|
|
1080
|
+
// one finite deadline; legacy closes carry no deadline. The persisted close
|
|
1081
|
+
// stamp bounds a future value in `normalizeStored`, where it can fail closed
|
|
1082
|
+
// without making the whole record unreadable.
|
|
1031
1083
|
if (r.restarting !== undefined && r.restarting !== true) return false;
|
|
1084
|
+
if (r.restartUntil !== undefined && (!isFiniteNumber(r.restartUntil) || r.restarting !== true)) return false;
|
|
1032
1085
|
if (typeof r.channelId !== "string" || typeof r.userId !== "string" || typeof r.threadKey !== "string") return false;
|
|
1033
1086
|
if (r.relayedBy !== undefined && typeof r.relayedBy !== "string") return false;
|
|
1034
1087
|
if (r.authenticatedAs !== undefined && typeof r.authenticatedAs !== "string") return false;
|