@coreplane/switchboard 1.260.6 → 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.
@@ -134,6 +134,8 @@ import {
134
134
  type IntakeWriteResult,
135
135
  type LivePhase,
136
136
  type LiveRunRow,
137
+ type LiveStateAssignRequest,
138
+ type LiveStateAssignResult,
137
139
  type ReclaimedRun,
138
140
  type RunState,
139
141
  type SessionHit,
@@ -154,6 +156,7 @@ import { isRunMetricsPoint, pointTurnsFinal, type RunMetricsPoint } from "../../
154
156
  import { AnalyticsEngineSink, NullSink, type RunMetricsSink } from "./runMetricsSink.ts";
155
157
  import { injectedBuildStamp } from "../../src/deploy/buildStamp.ts";
156
158
  import { systemClock } from "../../src/core/trace/clock.ts";
159
+ import { assignRunLiveState } from "../../src/core/runLiveState.ts";
157
160
  import { createTracer } from "../../src/core/trace/tracer.ts";
158
161
  import { startAdoptedRoot, workerLogSink } from "../../src/core/trace/workerTrace.ts";
159
162
 
@@ -1539,6 +1542,9 @@ type LiveRow = {
1539
1542
  };
1540
1543
 
1541
1544
  function rowToLive(r: LiveRow): LiveRunRow {
1545
+ const state = JSON.parse(r.state_json) as RunState;
1546
+ const liveState = state.liveState as LiveRunRow["liveState"];
1547
+ const liveStateSeq = state.liveStateSeq;
1542
1548
  return {
1543
1549
  runId: r.run_id,
1544
1550
  threadKey: r.thread_key,
@@ -1551,7 +1557,9 @@ function rowToLive(r: LiveRow): LiveRunRow {
1551
1557
  card: r.card_json ? (JSON.parse(r.card_json) as LiveRunRow["card"]) : null,
1552
1558
  system: r.system_text,
1553
1559
  tools: JSON.parse(r.tools_json) as LiveRunRow["tools"],
1554
- state: JSON.parse(r.state_json) as RunState,
1560
+ state,
1561
+ ...(liveState !== undefined ? { liveState } : {}),
1562
+ ...(typeof liveStateSeq === "number" ? { liveStateSeq } : {}),
1555
1563
  };
1556
1564
  }
1557
1565
 
@@ -3078,6 +3086,78 @@ export class RunHistoryDO extends DurableObject<Env> {
3078
3086
  return out;
3079
3087
  }
3080
3088
 
3089
+ async assignLiveState(
3090
+ runId: string,
3091
+ gen: string,
3092
+ assignment: LiveStateAssignRequest,
3093
+ ): Promise<LiveStateAssignResult> {
3094
+ let out: LiveStateAssignResult = { ok: false, reason: "unknown-run" };
3095
+ this.ctx.storage.transactionSync(() => {
3096
+ const row = this.liveRow(runId);
3097
+ const fence = checkFence(row, gen);
3098
+ if (!fence.ok) {
3099
+ out = fence;
3100
+ return;
3101
+ }
3102
+ if (!row) {
3103
+ out = { ok: false, reason: "unknown-run" };
3104
+ return;
3105
+ }
3106
+ const result = assignRunLiveState(
3107
+ assignment.restart ? undefined : row.liveState,
3108
+ row.liveStateSeq ?? 0,
3109
+ assignment,
3110
+ );
3111
+ if (!result.ok) {
3112
+ out = result;
3113
+ return;
3114
+ }
3115
+ let liveStateSeq = row.liveStateSeq ?? 0;
3116
+ const last = this.sql
3117
+ .exec<{ m: number | null }>(`SELECT MAX(seq) AS m FROM run_events WHERE run_id = ?`, runId)
3118
+ .one().m;
3119
+ let lastEventSeq = last ?? 0;
3120
+ for (const source of assignment.sourceEvents ?? []) {
3121
+ if (source.seq <= lastEventSeq) {
3122
+ out = { ok: false, reason: "stale-sequence" };
3123
+ return;
3124
+ }
3125
+ lastEventSeq = source.seq;
3126
+ }
3127
+ const boundarySeq = result.event ? (assignment.eventSeq ?? lastEventSeq + 1) : undefined;
3128
+ if (boundarySeq !== undefined && boundarySeq <= lastEventSeq) {
3129
+ out = { ok: false, reason: "stale-sequence" };
3130
+ return;
3131
+ }
3132
+ for (const source of assignment.sourceEvents ?? []) {
3133
+ this.sql.exec(
3134
+ `INSERT INTO run_events (run_id, seq, json) VALUES (?, ?, ?)`,
3135
+ runId,
3136
+ source.seq,
3137
+ JSON.stringify(source),
3138
+ );
3139
+ liveStateSeq = source.seq;
3140
+ }
3141
+ if (result.event && boundarySeq !== undefined) {
3142
+ liveStateSeq = boundarySeq;
3143
+ this.sql.exec(
3144
+ `INSERT INTO run_events (run_id, seq, json) VALUES (?, ?, ?)`,
3145
+ runId,
3146
+ liveStateSeq,
3147
+ JSON.stringify({ ...result.event, seq: liveStateSeq }),
3148
+ );
3149
+ }
3150
+ const state = { ...row.state, ...assignment.statePatch, liveState: result.liveState, liveStateSeq };
3151
+ this.sql.exec(`UPDATE live_runs SET state_json = ? WHERE run_id = ?`, JSON.stringify(state), runId);
3152
+ out = {
3153
+ ...result,
3154
+ ...(result.event ? { event: { ...result.event, seq: liveStateSeq } } : {}),
3155
+ liveStateSeq,
3156
+ };
3157
+ });
3158
+ return out;
3159
+ }
3160
+
3081
3161
  async setState(runId: string, gen: string, state: RunState): Promise<FenceResult> {
3082
3162
  let out: FenceResult = { ok: true };
3083
3163
  this.ctx.storage.transactionSync(() => {
@@ -5116,6 +5196,7 @@ const LEDGER_ROUTES = new Set([
5116
5196
  "/runs/claim",
5117
5197
  "/runs/heartbeat",
5118
5198
  "/runs/append",
5199
+ "/runs/live-state",
5119
5200
  "/runs/step",
5120
5201
  "/runs/state",
5121
5202
  "/runs/inbox",
@@ -5954,6 +6035,13 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
5954
6035
  console.log(`[runs/append] ${key.value} ${runId.value} <- ${events.length} event(s), ok=${r.ok}`);
5955
6036
  return fenced(r);
5956
6037
  }
6038
+ if (pathname === "/runs/live-state") {
6039
+ if (typeof b.assignment !== "object" || b.assignment === null)
6040
+ return json({ error: "assignment must be an object" }, 400);
6041
+ const assignment = b.assignment as unknown as LiveStateAssignRequest;
6042
+ const r = await stub.assignLiveState(runId.value, g.value, assignment);
6043
+ return r.ok ? json(r) : json(r, r.reason === "fenced" || r.reason === "unknown-run" ? 409 : 400);
6044
+ }
5957
6045
  if (pathname === "/runs/step") {
5958
6046
  const record = parseStep(b.record);
5959
6047
  if (!record.ok) return json({ error: record.error }, 400);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.260.6",
3
+ "version": "1.261.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.260.6",
9
+ "version": "1.261.0",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -21060,7 +21060,7 @@
21060
21060
  },
21061
21061
  "packages/switchboard": {
21062
21062
  "name": "@coreplane/switchboard",
21063
- "version": "1.260.6",
21063
+ "version": "1.261.0",
21064
21064
  "license": "Apache-2.0",
21065
21065
  "dependencies": {
21066
21066
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.260.6",
3
+ "version": "1.261.0",
4
4
  "private": true,
5
5
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.260.6",
3
- "commit": "903ae2855833637fbcc0c0a22d54e7268be75965",
4
- "builtAt": "2026-09-23T04:57:07.101Z"
2
+ "version": "1.261.0",
3
+ "commit": "c60b26943f774d0ba0e71af0df8db7d1d90613c5",
4
+ "builtAt": "2026-09-23T08:28:35.014Z"
5
5
  }
@@ -12,6 +12,30 @@ import type { UnitEnding } from "./ship/coordinator.js";
12
12
  /** The stage vocabulary (record 0065): what every surface says a unit is in. */
13
13
  export type Stage = "coding" | "review" | "fix" | "approved" | "merge-ready" | "merged" | "idle" | "ended";
14
14
 
15
+ export const HOSTED_PIPELINE_STARTING_DETAIL = "pipeline starting";
16
+
17
+ /** Safe detail for a hosted parent's shared `working` state, total over Stage. */
18
+ export function hostedStageDetail(stage: Stage): string {
19
+ switch (stage) {
20
+ case "coding":
21
+ return "coding a unit";
22
+ case "review":
23
+ return "reviewing a unit";
24
+ case "fix":
25
+ return "fixing review findings";
26
+ case "approved":
27
+ return "waiting for checks and merge";
28
+ case "merge-ready":
29
+ return "ready for a person to merge";
30
+ case "merged":
31
+ return "a unit merged";
32
+ case "idle":
33
+ return "waiting for the next pipeline step";
34
+ case "ended":
35
+ return "finishing the pipeline";
36
+ }
37
+ }
38
+
15
39
  export const STAGES = [
16
40
  "coding",
17
41
  "review",
@@ -12,6 +12,31 @@
12
12
  import { PLANE, minutesToMs } from "../budgets.js";
13
13
  import type { ProviderFailureCause } from "../provider.js";
14
14
  import type { PlaneFinding } from "./findings.js";
15
+ import type { RunLiveStateName } from "../runLiveState.js";
16
+
17
+ /** The one user wording table for the server-owned live condition. */
18
+ export function liveStateWords(state: RunLiveStateName): string {
19
+ switch (state) {
20
+ case "admitted":
21
+ return "admitted";
22
+ case "waiting_deploy":
23
+ return "waiting for the current deploy";
24
+ case "waiting_repository":
25
+ return "waiting for the repository container";
26
+ case "falling_back":
27
+ return "switching to a fallback workspace";
28
+ case "preparing":
29
+ return "preparing the workspace";
30
+ case "working":
31
+ return "working";
32
+ case "waiting_provider":
33
+ return "waiting for the model provider";
34
+ case "wrapping_up":
35
+ return "wrapping up";
36
+ case "ended":
37
+ return "ended";
38
+ }
39
+ }
15
40
 
16
41
  // ---- endings and their causes (record 0064, "Endings and the watches") ------------------------
17
42
 
@@ -5,6 +5,7 @@ import type { DescriptionIssue, PrDescription, RecordedJson, RenderedPointer } f
5
5
  import type { HarnessScope } from "./harness/scope.js";
6
6
  import type { ModelCard } from "./modelCard.js";
7
7
  import type { ProviderFailureCause } from "./provider.js";
8
+ import type { RunStateEvent } from "./runLiveState.js";
8
9
 
9
10
  /** The `pr_description` review artifact minus the event envelope
10
11
  * (docs/reference/specs/reading-diff.md item 7). */
@@ -492,6 +493,7 @@ export function isHeadMaterial(event: RunEvent): boolean {
492
493
  case "context":
493
494
  case "reference":
494
495
  case "run_meta":
496
+ case "run_state":
495
497
  case "route":
496
498
  return true;
497
499
  case "run_note":
@@ -923,6 +925,9 @@ export type RunEvent =
923
925
  * start of the loop; a resumed run carries the original. Head material,
924
926
  * like `run_meta`. Additive: unknown → ignored. */
925
927
  | { type: "lease"; startedAt: number; endsAt: number; loopEndsAt: number; seq?: number; at?: number }
928
+ /** A server-owned live-condition boundary. Same-state refreshes update the
929
+ * durable projection beside their source event and do not emit this event. */
930
+ | RunStateEvent
926
931
  /** A head the run pushed (docs/reference/specs/run-history.md item 2; decision
927
932
  * 0046): the branch and the sha the coding post-step observed on the remote
928
933
  * (`by: "push"`), or the budget-end salvage pushed (`by: "salvage"`),
@@ -438,6 +438,7 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
438
438
  ev.type === "operator" ||
439
439
  ev.type === "refusal" ||
440
440
  ev.type === "reference" ||
441
+ ev.type === "run_state" ||
441
442
  ev.type === "lease" ||
442
443
  ev.type === "pushed_head"
443
444
  ) {
@@ -537,6 +538,7 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
537
538
  return;
538
539
  }
539
540
 
541
+ if (ev.type !== "run_note") return;
540
542
  switch (ev.kind) {
541
543
  case "wrap_up":
542
544
  // Its extent is the time from the warning to the end of the window (how
@@ -9,6 +9,7 @@ import type { ChannelVisibility } from "../authz/types.js";
9
9
  import type { RunProfile } from "../../config/profile.js";
10
10
  import type { RunEvent } from "../runEvents.js";
11
11
  import type { RunSeed, RunSession } from "../runRecord.js";
12
+ import type { AssignRunLiveStateInput, AssignRunLiveStateResult, RunLiveState } from "../runLiveState.js";
12
13
 
13
14
  /** How long a generation's claim on a run lasts without a heartbeat. */
14
15
  export const LEASE_MS = 30_000;
@@ -158,8 +159,28 @@ export interface LiveRunRow {
158
159
  /** The tool definitions the run was started with, verbatim. */
159
160
  tools: ToolDef[];
160
161
  state: RunState;
162
+ /** The restart-safe live-condition projection. Optional on legacy rows. */
163
+ liveState?: RunLiveState;
164
+ /** Stream sequence that most recently produced or refreshed `liveState`. */
165
+ liveStateSeq?: number;
161
166
  }
162
167
 
168
+ /** The atomic live-condition assignment request and answer shared by both ledger implementations. */
169
+ export type LiveStateAssignRequest = AssignRunLiveStateInput & {
170
+ /** Registry sequence reserved for a changed boundary before fan-out. */
171
+ eventSeq?: number;
172
+ /** Other durable row facts that must commit with the projection (for example hosting). */
173
+ statePatch?: RunState;
174
+ /** Existing activity facts committed with a same-state projection refresh. */
175
+ sourceEvents?: AppendableEvent[];
176
+ /** A new execution segment under the same run id starts after an ended predecessor. */
177
+ restart?: boolean;
178
+ };
179
+ export type LiveStateAssignResult =
180
+ | (Extract<AssignRunLiveStateResult, { ok: true }> & { liveStateSeq: number })
181
+ | Extract<AssignRunLiveStateResult, { ok: false }>
182
+ | { ok: false; reason: "fenced" | "unknown-run" };
183
+
163
184
  /** One tool call the step dispatched; `tool` decides how a resume settles it.
164
185
  * `boundMs` is the bound the call declared (a bash `timeout`), when it stated
165
186
  * one — what the plane judges a `long_call` steer against (record 0064). */
@@ -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