@coreplane/switchboard 1.260.5 → 1.261.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 (40) hide show
  1. package/dist/assets/deploy/cloudflare-memory/worker.ts +106 -4
  2. package/dist/assets/package-lock.json +3 -3
  3. package/dist/assets/package.json +1 -1
  4. package/dist/assets/source.json +3 -3
  5. package/dist/assets/src/core/budgets.ts +5 -0
  6. package/dist/assets/src/core/coordinator/driver.ts +43 -6
  7. package/dist/assets/src/core/pipelineStanding.ts +24 -0
  8. package/dist/assets/src/core/plane/decide.ts +25 -0
  9. package/dist/assets/src/core/refusal.ts +1 -1
  10. package/dist/assets/src/core/runEvents.ts +5 -0
  11. package/dist/assets/src/core/runFriction.ts +2 -0
  12. package/dist/assets/src/core/runLedger/decisions.ts +10 -1
  13. package/dist/assets/src/core/runLedger/types.ts +24 -0
  14. package/dist/assets/src/core/runLiveState.ts +205 -0
  15. package/dist/assets/src/core/runRecord.ts +4 -0
  16. package/dist/assets/src/core/ship/coordinator.ts +29 -6
  17. package/dist/assets/web/dist/.vite/manifest.json +67 -67
  18. package/dist/assets/web/dist/assets/{DeliveryPage-Cf1joUyA.js → DeliveryPage-BUJckSuW.js} +1 -1
  19. package/dist/assets/web/dist/assets/{HomePage-DZEAnCCv.js → HomePage-CBQpcnaW.js} +1 -1
  20. package/dist/assets/web/dist/assets/{PendingTurnRow-4B8JwETU.js → PendingTurnRow-DIC86uWA.js} +1 -1
  21. package/dist/assets/web/dist/assets/{PlanePage-CHU2szQs.js → PlanePage-CfrbMh8t.js} +1 -1
  22. package/dist/assets/web/dist/assets/{ResidentDetailPage-L5SRcN_O.js → ResidentDetailPage-C-SgOUTl.js} +1 -1
  23. package/dist/assets/web/dist/assets/{ResidentsIndexPage-BIFf8qyE.js → ResidentsIndexPage-CiDhvXok.js} +1 -1
  24. package/dist/assets/web/dist/assets/{RunFoldRow-DzPIshJx.js → RunFoldRow-DZ8huwUU.js} +1 -1
  25. package/dist/assets/web/dist/assets/{RunRoutePage-Cru5IVux.js → RunRoutePage-Cp6wicYc.js} +3 -3
  26. package/dist/assets/web/dist/assets/{RunsIndexPage-CBkdPAdj.js → RunsIndexPage-BPXtfwwx.js} +1 -1
  27. package/dist/assets/web/dist/assets/{ScheduledPage-D_R55BDk.js → ScheduledPage-DbvP48HI.js} +1 -1
  28. package/dist/assets/web/dist/assets/{SettingsPage-DlmZvAvf.js → SettingsPage-B87lZaiN.js} +1 -1
  29. package/dist/assets/web/dist/assets/{SilentTurn-PWMDMbMG.js → SilentTurn-CPED_XCV.js} +1 -1
  30. package/dist/assets/web/dist/assets/{StatusDot-Bq0mYMFo.js → StatusDot-CFLn-DZ2.js} +1 -1
  31. package/dist/assets/web/dist/assets/{Tooltip-Bs6zd7yE.js → Tooltip-Dx9YBVEx.js} +1 -1
  32. package/dist/assets/web/dist/assets/{UnitRoutePage-BMGEZCvn.js → UnitRoutePage-d4A2X_o7.js} +1 -1
  33. package/dist/assets/web/dist/assets/budgets-BQgcCyvb.js +1 -0
  34. package/dist/assets/web/dist/assets/{dist-EkKVbIM-.js → dist-Cn51vWpw.js} +1 -1
  35. package/dist/assets/web/dist/assets/{indexRow-5s3hNzZ9.js → indexRow-BsWcHHVh.js} +1 -1
  36. package/dist/assets/web/dist/assets/{main-Disot0n_.js → main-DYwRHh0F.js} +2 -2
  37. package/dist/assets/web/dist/assets/{sseReplay-9gcMpylx.js → sseReplay-DGRDRLua.js} +1 -1
  38. package/dist/cli.js +1673 -722
  39. package/package.json +1 -1
  40. package/dist/assets/web/dist/assets/budgets-0szUBIMv.js +0 -1
@@ -0,0 +1,205 @@
1
+ import type { PlaneEndingCause } from "./plane/decide.js";
2
+ import { oneLine, redactAndCap } from "./redact.js";
3
+
4
+ /** The server-owned condition of an admitted run. A plane queue row is not admitted and has none. */
5
+ export type RunLiveStateName =
6
+ | "admitted"
7
+ | "waiting_deploy"
8
+ | "waiting_repository"
9
+ | "falling_back"
10
+ | "preparing"
11
+ | "working"
12
+ | "waiting_provider"
13
+ | "wrapping_up"
14
+ | "ended";
15
+
16
+ export const RUN_LIVE_STATE_NAMES = [
17
+ "admitted",
18
+ "waiting_deploy",
19
+ "waiting_repository",
20
+ "falling_back",
21
+ "preparing",
22
+ "working",
23
+ "waiting_provider",
24
+ "wrapping_up",
25
+ "ended",
26
+ ] as const satisfies readonly RunLiveStateName[];
27
+
28
+ export const LIVE_STATE_DETAIL_MAX = 160;
29
+
30
+ export type ResidentLiveStateObservation =
31
+ | { state: "waiting_deploy"; bound: number; reason: "deploy"; attempt: number }
32
+ | { state: "waiting_repository"; bound: number; reason: "repository_container"; attempt: number };
33
+
34
+ export type ResidentLiveStateObserver = (observation: ResidentLiveStateObservation) => Promise<void>;
35
+
36
+ export interface RunLiveState {
37
+ state: RunLiveStateName;
38
+ /** Epoch ms at the most recent state boundary. Same-state refreshes preserve it. */
39
+ since: number;
40
+ /** Absolute epoch-ms deadline of the durable fact that created this live condition. */
41
+ bound?: number;
42
+ /** Capped, redacted user words. Never a provider body or resident refusal code. */
43
+ detail?: string;
44
+ }
45
+
46
+ export interface RunStateEvent extends RunLiveState {
47
+ type: "run_state";
48
+ /** Required only for the terminal boundary; it names the plane's existing closed cause. */
49
+ cause?: PlaneEndingCause;
50
+ seq?: number;
51
+ at?: number;
52
+ }
53
+
54
+ export interface RunLiveStateFoldEvent {
55
+ type: string;
56
+ seq?: number;
57
+ at?: number;
58
+ state?: unknown;
59
+ since?: number;
60
+ bound?: number;
61
+ detail?: string;
62
+ endsAt?: number;
63
+ boundMs?: number;
64
+ }
65
+
66
+ export interface RunLiveStateMaterialized {
67
+ liveState?: RunLiveState;
68
+ /** Authoritative event sequence that produced or refreshed `liveState`. */
69
+ liveStateSeq?: number;
70
+ }
71
+
72
+ const TRANSITIONS = {
73
+ admitted: ["waiting_deploy", "waiting_repository", "falling_back", "preparing", "working", "wrapping_up", "ended"],
74
+ waiting_deploy: ["waiting_repository", "falling_back", "preparing", "working", "wrapping_up", "ended"],
75
+ waiting_repository: ["waiting_deploy", "falling_back", "preparing", "working", "wrapping_up", "ended"],
76
+ falling_back: ["preparing", "working", "wrapping_up", "ended"],
77
+ preparing: ["waiting_deploy", "waiting_repository", "falling_back", "working", "wrapping_up", "ended"],
78
+ working: [
79
+ "waiting_deploy",
80
+ "waiting_repository",
81
+ "falling_back",
82
+ "preparing",
83
+ "waiting_provider",
84
+ "wrapping_up",
85
+ "ended",
86
+ ],
87
+ waiting_provider: ["working", "wrapping_up", "ended"],
88
+ wrapping_up: ["ended"],
89
+ ended: [],
90
+ } as const satisfies Record<RunLiveStateName, readonly RunLiveStateName[]>;
91
+
92
+ const UNSAFE_DETAIL =
93
+ /(?:\bimage-stale\b|\battach-failed\b|\bparked-provider\b|<\/?(?:html|body|head)\b|\bbad gateway\b)/i;
94
+
95
+ /** Admit only display-ready prose. Typed mappers create normal details; this is the final leak guard. */
96
+ export function safeLiveStateDetail(detail: string | undefined): string | undefined {
97
+ if (detail === undefined) return undefined;
98
+ const line = oneLine(detail);
99
+ if (line === "" || UNSAFE_DETAIL.test(line)) return undefined;
100
+ return redactAndCap(line, LIVE_STATE_DETAIL_MAX - 1);
101
+ }
102
+
103
+ export type AssignRunLiveStateInput = {
104
+ expectedSeq: number;
105
+ at: number;
106
+ state: RunLiveStateName;
107
+ bound?: number;
108
+ detail?: string;
109
+ cause?: PlaneEndingCause;
110
+ };
111
+
112
+ export type AssignRunLiveStateResult =
113
+ | { ok: true; liveState: RunLiveState; event?: RunStateEvent }
114
+ | {
115
+ ok: false;
116
+ reason:
117
+ "stale-sequence" | "terminal" | "invalid-transition" | "bound-required" | "invalid-bound" | "cause-required";
118
+ };
119
+
120
+ /** Pure assignment gate. Persistence chooses the next stream sequence and commits the result atomically. */
121
+ export function assignRunLiveState(
122
+ current: RunLiveState | undefined,
123
+ currentSeq: number,
124
+ input: AssignRunLiveStateInput,
125
+ ): AssignRunLiveStateResult {
126
+ if (input.expectedSeq !== currentSeq) return { ok: false, reason: "stale-sequence" };
127
+ if (current === undefined && input.state !== "admitted") return { ok: false, reason: "invalid-transition" };
128
+ if (current?.state === "ended") return { ok: false, reason: "terminal" };
129
+ if (input.state !== "ended") {
130
+ if (input.bound === undefined) return { ok: false, reason: "bound-required" };
131
+ if (!Number.isFinite(input.bound) || input.bound < input.at) return { ok: false, reason: "invalid-bound" };
132
+ } else if (input.cause === undefined) return { ok: false, reason: "cause-required" };
133
+ if (
134
+ current !== undefined &&
135
+ current.state !== input.state &&
136
+ !TRANSITIONS[current.state].includes(input.state as never)
137
+ )
138
+ return { ok: false, reason: "invalid-transition" };
139
+
140
+ const detail = safeLiveStateDetail(input.detail);
141
+ const liveState: RunLiveState = {
142
+ state: input.state,
143
+ since: current?.state === input.state ? current.since : input.at,
144
+ ...(input.state !== "ended" ? { bound: input.bound } : {}),
145
+ ...(detail !== undefined ? { detail } : {}),
146
+ };
147
+ if (current?.state === input.state) return { ok: true, liveState };
148
+ return {
149
+ ok: true,
150
+ liveState,
151
+ event: {
152
+ type: "run_state",
153
+ ...liveState,
154
+ ...(input.state === "ended" ? { cause: input.cause } : {}),
155
+ at: input.at,
156
+ },
157
+ };
158
+ }
159
+
160
+ /** Rebuild the projection from boundaries. A later event sequence always defeats stale materialized state. */
161
+ export function foldRunLiveState(
162
+ events: readonly RunLiveStateFoldEvent[],
163
+ materialized: RunLiveStateMaterialized = {},
164
+ ): RunLiveStateMaterialized {
165
+ let liveState = materialized.liveState;
166
+ let liveStateSeq = materialized.liveStateSeq ?? 0;
167
+ let leaseEndsAt: number | undefined;
168
+ for (const event of [...events].sort((left, right) => (left.seq ?? 0) - (right.seq ?? 0))) {
169
+ if (event.type === "lease" && event.endsAt !== undefined) leaseEndsAt = event.endsAt;
170
+ const seq = event.seq ?? 0;
171
+ if (seq <= liveStateSeq) continue;
172
+ if (
173
+ event.type === "run_state" &&
174
+ RUN_LIVE_STATE_NAMES.includes(event.state as RunLiveStateName) &&
175
+ event.since !== undefined
176
+ ) {
177
+ liveState = {
178
+ state: event.state as RunLiveStateName,
179
+ since: event.since,
180
+ ...(event.bound !== undefined ? { bound: event.bound } : {}),
181
+ ...(event.detail !== undefined ? { detail: event.detail } : {}),
182
+ };
183
+ liveStateSeq = seq;
184
+ continue;
185
+ }
186
+ if (
187
+ event.type === "tool_call" &&
188
+ liveState?.state === "working" &&
189
+ event.at !== undefined &&
190
+ event.boundMs !== undefined
191
+ ) {
192
+ liveState = { ...liveState, bound: event.at + event.boundMs, detail: "running a tool" };
193
+ liveStateSeq = seq;
194
+ continue;
195
+ }
196
+ if (event.type === "tool_result" && liveState?.state === "working" && leaseEndsAt !== undefined) {
197
+ liveState = { ...liveState, bound: leaseEndsAt, detail: "model turn" };
198
+ liveStateSeq = seq;
199
+ }
200
+ }
201
+ return {
202
+ ...(liveState !== undefined ? { liveState } : {}),
203
+ ...(liveStateSeq > 0 ? { liveStateSeq } : {}),
204
+ };
205
+ }
@@ -5,6 +5,7 @@ import type { ProviderFailureCause } from "./provider.js";
5
5
  import { isHeadMaterial, isSpanRecord } from "./runEvents.js";
6
6
  import { isRunUsage, type RunUsage } from "./runUsage.js";
7
7
  import type { PushedBranch } from "../execution/residentRebind.js";
8
+ import type { RunLiveState } from "./runLiveState.js";
8
9
  import { isHandoffShape, type Handoff } from "./ship/handoff.js";
9
10
  import {
10
11
  type FindingDisposition,
@@ -124,6 +125,9 @@ export interface RunRecord {
124
125
  stepCount?: number;
125
126
  schema?: number;
126
127
  status: RunStatus;
128
+ /** Final server-owned live condition. Optional on records from older writers. */
129
+ liveState?: RunLiveState;
130
+ liveStateSeq?: number;
127
131
  /** Present on a tombstone record still in its provisional window — the
128
132
  * start-of-run `interrupted` or the drain-deadline upgrade — before the
129
133
  * run's final write lands. Absent on every final (finished) record. A
@@ -550,7 +550,7 @@ export type CoordinatorAction =
550
550
  | { type: "branch"; step: string; branch: string; from: string }
551
551
  | { type: "spawn"; step: string; preset: ChildPreset; round: RoundRef; budgetMinutes: number; brief: Brief }
552
552
  | { type: "wait"; step: string; runId: string; timeoutMs: number }
553
- | { type: "read-record"; step: string; runId: string }
553
+ | { type: "read-record"; step: string; runId: string; finishedObserved?: true }
554
554
  /** Tell a child whose pull request became terminal or moved to end at its
555
555
  * next turn boundary without publishing anything. This is a steer through
556
556
  * the child's inbox, never a stop that tears down its workspace. */
@@ -1170,7 +1170,7 @@ type Phase =
1170
1170
  | { at: "busy-wait"; round: RoundRef; runId?: string; n: number }
1171
1171
  /** `until`: when the child's budget plus the margin runs out, counted from the spawn's answer — the wait's last slice ends there. */
1172
1172
  | { at: "wait"; round: RoundRef; runId: string; n: number; until: number }
1173
- | { at: "read"; round: RoundRef; runId: string; n: number; until: number }
1173
+ | { at: "read"; round: RoundRef; runId: string; n: number; until: number; finishedObserved: boolean }
1174
1174
  /** A live child lost authority because its pull request became terminal,
1175
1175
  * its reviewed head moved, or its branch disappeared. Steer once, then
1176
1176
  * drain the child to its recorded end while ignoring every late artifact. */
@@ -1197,6 +1197,7 @@ type Phase =
1197
1197
  n: number;
1198
1198
  until: number;
1199
1199
  supersession: ChildSupersession;
1200
+ finishedObserved: boolean;
1200
1201
  }
1201
1202
  /** A moved-head child has drained. Re-read the adopted pull request before
1202
1203
  * restarting review because it may have merged, closed or moved again. */
@@ -1533,7 +1534,12 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
1533
1534
  timeoutMs: Math.min(waitSliceMs(s.clock, p.until), s.pr !== undefined ? PR_TRANSITION_GUARD_MS : WAIT_CHUNK_MS),
1534
1535
  };
1535
1536
  case "read":
1536
- return { type: "read-record", step: `${roundStep(s, p.round)}/read/${p.n}`, runId: p.runId };
1537
+ return {
1538
+ type: "read-record",
1539
+ step: `${roundStep(s, p.round)}/read/${p.n}`,
1540
+ runId: p.runId,
1541
+ ...(p.finishedObserved ? { finishedObserved: true as const } : {}),
1542
+ };
1537
1543
  case "steer":
1538
1544
  return {
1539
1545
  type: "steer",
@@ -1553,6 +1559,7 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
1553
1559
  type: "read-record",
1554
1560
  step: `${roundStep(s, p.round)}/superseded/read/${p.n}`,
1555
1561
  runId: p.runId,
1562
+ ...(p.finishedObserved ? { finishedObserved: true as const } : {}),
1556
1563
  };
1557
1564
  case "superseded-pr-check":
1558
1565
  return {
@@ -2960,11 +2967,23 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
2960
2967
  notes: [],
2961
2968
  };
2962
2969
  }
2963
- case "wait":
2970
+ case "wait": {
2971
+ const r = ret as Extract<StepReturn, { type: "wait" }>;
2964
2972
  return {
2965
- state: { ...s, phase: { at: "read", round: p.round, runId: p.runId, n: p.n, until: p.until } },
2973
+ state: {
2974
+ ...s,
2975
+ phase: {
2976
+ at: "read",
2977
+ round: p.round,
2978
+ runId: p.runId,
2979
+ n: p.n,
2980
+ until: p.until,
2981
+ finishedObserved: r.outcome === "event",
2982
+ },
2983
+ },
2966
2984
  notes: [],
2967
2985
  };
2986
+ }
2968
2987
  case "read": {
2969
2988
  const r = ret as Extract<StepReturn, { type: "read-record" }>;
2970
2989
  const supersession = childSupersession(clocked, p.round, r.pullRequest);
@@ -2979,6 +2998,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
2979
2998
  n: p.n,
2980
2999
  until: p.until,
2981
3000
  supersession,
3001
+ finishedObserved: p.finishedObserved,
2982
3002
  };
2983
3003
  return finishSupersededChild(clocked, terminalPhase);
2984
3004
  }
@@ -3098,7 +3118,8 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
3098
3118
  notes: [],
3099
3119
  };
3100
3120
  }
3101
- case "superseded-wait":
3121
+ case "superseded-wait": {
3122
+ const r = ret as Extract<StepReturn, { type: "wait" }>;
3102
3123
  return {
3103
3124
  state: {
3104
3125
  ...s,
@@ -3109,10 +3130,12 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
3109
3130
  n: p.n,
3110
3131
  until: p.until,
3111
3132
  supersession: p.supersession,
3133
+ finishedObserved: r.outcome === "event",
3112
3134
  },
3113
3135
  },
3114
3136
  notes: [],
3115
3137
  };
3138
+ }
3116
3139
  case "superseded-read": {
3117
3140
  const r = ret as Extract<StepReturn, { type: "read-record" }>;
3118
3141
  if (r.run.finished) return finishSupersededChild(clocked, p);