Problem
Contract 1.7.0 added checkoutProvenance to OperatorRunStatus, and 1.8.0 added checkoutPreparation. toOperatorRunStatus populates both. The SSE status projection then rebuilds the frame from a fixed field list that predates both, so neither field ever reaches operator stream consumers.
packages/gateway/src/web/sse/projection.ts (projectRunObservation):
// Copy only the contract fields; never spread runState or read its details.
const result: OperatorRunStatus = {
runId: base.runId,
entityRef: base.entityRef,
surface: base.surface,
phase: base.phase,
status: overlaidStatus,
startedAt: base.startedAt,
stale: base.stale,
...(base.failureKind === undefined ? {} : {failureKind: base.failureKind}),
}
base is the deny-gated result of projectRunStatus, which already carries both fields when present. They are dropped here. This is unchanged from v0.118.1 through current main.
Why it matters
The checkout update/recovery plan (docs/plans/2026-09-24-001-feat-workspace-checkout-update-recovery-plan.md) says:
- line 50: "the web surface only displays the new contract fields in this plan"
- line 437 (Unit 6 goal): "Carry the preparation result through the gateway and to operators"
- line 614: "refusals and failures are persisted with
checkoutPreparation; the operator surface shows them"
The run stream is the operator surface's only channel for OperatorRunStatus; run summaries are a separate, leaner DTO. As shipped, a run refused by checkout preparation reaches the operator as a bare FAILED with no failureKind and no preparation record, and a run that started has no visible provenance. Dashboard consumers adopting 1.8.0 have nothing to render.
Expected
The projection copies checkoutProvenance and checkoutPreparation from base, conditionally like failureKind, so the deny gate still applies first. A projection test pins that both fields appear on the status frame when present and are absent otherwise.
Problem
Contract 1.7.0 added
checkoutProvenancetoOperatorRunStatus, and 1.8.0 addedcheckoutPreparation.toOperatorRunStatuspopulates both. The SSE status projection then rebuilds the frame from a fixed field list that predates both, so neither field ever reaches operator stream consumers.packages/gateway/src/web/sse/projection.ts(projectRunObservation):baseis the deny-gated result ofprojectRunStatus, which already carries both fields when present. They are dropped here. This is unchanged from v0.118.1 through currentmain.Why it matters
The checkout update/recovery plan (
docs/plans/2026-09-24-001-feat-workspace-checkout-update-recovery-plan.md) says:checkoutPreparation; the operator surface shows them"The run stream is the operator surface's only channel for
OperatorRunStatus; run summaries are a separate, leaner DTO. As shipped, a run refused by checkout preparation reaches the operator as a bareFAILEDwith nofailureKindand no preparation record, and a run that started has no visible provenance. Dashboard consumers adopting 1.8.0 have nothing to render.Expected
The projection copies
checkoutProvenanceandcheckoutPreparationfrombase, conditionally likefailureKind, so the deny gate still applies first. A projection test pins that both fields appear on the status frame when present and are absent otherwise.