Release Orchestrator · First Signal API

Provides a fast “first meaningful signal” for a run — the time-to-first-signal (TTFS) read — backed by a per-run snapshot, with weak-ETag conditional requests.

Audience: UI and CLI authors polling run progress, and integrators wiring early run telemetry.

Implemented by the Release Orchestrator service (StellaOps.ReleaseOrchestrator.WebApi, FirstSignalEndpointsFirstSignalService). The signal is snapshot-only: this service does not compute signals from runs/jobs. It returns whatever snapshot an upstream producer has written via FirstSignalService.UpdateSnapshotAsync (persisted in release_orchestrator.first_signal_snapshots). There is no in-process cache layer, no cold-start computation path, and no background snapshot writer in this service.

Endpoint

Canonical route:

GET /api/v1/release-orchestrator/runs/{runId}/first-signal

Legacy compatibility alias (the gateway still routes UI /api/v1/jobengine/* paths to this service; JobEngineLegacyEndpoints registers the alias and delegates to the same handler):

GET /api/v1/jobengine/runs/{runId}/first-signal

{runId} is constrained to a GUID ({runId:guid}); a non-GUID segment does not match this route.

Authorization

Required headers

Optional headers

Responses

200 OK

Returns the first signal payload and a weak ETag.

Response headers:

Body (application/json) — shape of FirstSignalResponse / FirstSignalDto:

{
  "runId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
  "firstSignal": {
    "type": "started",
    "stage": "resolve",
    "step": null,
    "message": "Run started",
    "at": "2025-12-15T12:00:10+00:00",
    "artifact": { "kind": "image", "range": null },
    "lastKnownOutcome": null
  },
  "summaryEtag": "W/\"...\""
}

Field notes (grounded in FirstSignalEndpoints.MapToResponse and FirstSignalDto):

Note: when a signal is present, firstSignal is non-null. firstSignal is serialized as null only via the MapToResponse branch where result.Signal is null, which is not reachable for a 200 in the current FirstSignalService (a found snapshot always carries a deserialized signal).

304 Not Modified

Returned when If-None-Match matches the current weak ETag (exact match, or *).

404 Not Found

Returned when no first-signal snapshot exists for the resolved tenant + run, or when the stored snapshot JSON is unparseable (the service deletes the bad row and returns 404). This is the “no signal available yet” case for the snapshot-only service.

400 Bad Request

Missing/blank X-Tenant-Id header, an empty run GUID, or other invalid argument. Body shape: { "error": "<message>" }.

Not currently returned: 204 No Content. The result-status enum defines a NotAvailable state that maps to 204, but the snapshot-only FirstSignalService never produces it — the absence of a snapshot is reported as 404 Not Found. Treat 204 as reserved/forward-looking.

ETag semantics

Stored snapshot model

The snapshot the producer writes (and the GET reads) is persisted in PostgreSQL table release_orchestrator.first_signal_snapshots (migration 001_initial.sql), keyed by (tenant_id, run_id):

ColumnNotes
tenant_id, run_idcomposite primary key
job_idrun’s job GUID
created_at, updated_attimestamps
kindCHECK ∈ queued, started, phase, blocked, failed, succeeded, canceled, unavailable
phaseCHECK ∈ resolve, fetch, restore, analyze, policy, report, unknown
summary, eta_secondsscalar fields
last_known_outcome, next_actions, diagnosticsJSONB
signal_jsonfull canonical signal document used for ETag/return

The underlying FirstSignal domain record also carries version (default "1.0"), signalId, scope { type, id }, and nextActions[] { type, label, target }, which are persisted in signal_json but are not all surfaced in the REST response DTO.

Streaming (SSE) — NOT IMPLEMENTED in this service

The Server-Sent Events stream described in earlier drafts (GET /api/v1/jobengine/stream/runs/{runId} emitting first_signal events) is not implemented by the Release Orchestrator. The legacy /api/v1/jobengine/stream/* prefix is registered as an unsupported compatibility route in JobEngineLegacyEndpoints and returns 501 Not Implemented with body { "error": "not_implemented", "message": "...not yet been migrated to the release-orchestrator service.", "path": "...", "hint": "..." }. Run-stream / first_signal push delivery is tracked in the orchestrator decomposition plan and is not available today.

Configuration

There is no FirstSignal (Cache / ColdPath / SnapshotWriter) or messaging configuration section for this endpoint in the Release Orchestrator. The feature is snapshot-only and Postgres-backed; the relevant configuration is the service’s database connection (the release_orchestrator schema is auto-migrated on startup via AddReleaseOrchestratorPersistence). No in-memory/Valkey cache backend, cold-path timeout, or background snapshot-writer poller is wired in this service.

The FirstSignal.Cache / FirstSignal.ColdPath / FirstSignal.SnapshotWriter block documented in earlier drafts belonged to the legacy JobEngine implementation and does not apply to the Release Orchestrator service that now serves these routes.