@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.
- package/dist/assets/deploy/cloudflare-memory/worker.ts +89 -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/core/pipelineStanding.ts +24 -0
- package/dist/assets/src/core/plane/decide.ts +25 -0
- package/dist/assets/src/core/runEvents.ts +5 -0
- package/dist/assets/src/core/runFriction.ts +2 -0
- package/dist/assets/src/core/runLedger/types.ts +21 -0
- package/dist/assets/src/core/runLiveState.ts +205 -0
- package/dist/assets/src/core/runRecord.ts +4 -0
- package/dist/cli.js +945 -166
- package/package.json +1 -1
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
21063
|
+
"version": "1.261.0",
|
|
21064
21064
|
"license": "Apache-2.0",
|
|
21065
21065
|
"dependencies": {
|
|
21066
21066
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
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",
|
package/dist/assets/source.json
CHANGED
|
@@ -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
|