# Orchestrator · First Signal API Provides a fast “first meaningful signal” for a run (TTFS), with caching and ETag-based conditional requests. ## Endpoint `GET /api/v1/orchestrator/runs/{runId}/first-signal` ### Required headers - `X-Tenant-Id`: tenant identifier (string) ### Optional headers - `If-None-Match`: weak ETag from a previous 200 response (supports multiple values) ## Responses ### 200 OK Returns the first signal payload and a weak ETag. Response headers: - `ETag`: weak ETag (for `If-None-Match`) - `Cache-Control: private, max-age=60` - `Cache-Status: hit|miss` - `X-FirstSignal-Source: snapshot|cold_start` (best-effort diagnostics) Body (`application/json`): ```json { "runId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "firstSignal": { "type": "started", "stage": "unknown", "step": null, "message": "Run started", "at": "2025-12-15T12:00:10+00:00", "artifact": { "kind": "run", "range": null } }, "summaryEtag": "W/\"...\"" } ``` ### 204 No Content Run exists but no signal is available yet (e.g., run has no jobs). ### 304 Not Modified Returned when `If-None-Match` matches the current ETag. ### 404 Not Found Run does not exist for the resolved tenant. ### 400 Bad Request Missing/invalid tenant header or invalid parameters. ## ETag semantics - Weak ETags are computed from a deterministic, canonical hash of the stable signal content. - Per-request diagnostics (e.g., cache hit/miss) are intentionally excluded from the ETag material. ## Streaming (SSE) The run stream emits `first_signal` events when the signal changes: `GET /api/v1/orchestrator/stream/runs/{runId}` Event type: - `first_signal` Payload shape: ```json { "runId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "etag": "W/\"...\"", "signal": { "version": "1.0", "signalId": "...", "jobId": "...", "timestamp": "...", "kind": 1, "phase": 6, "scope": { "type": "run", "id": "..." }, "summary": "...", "etaSeconds": null, "lastKnownOutcome": null, "nextActions": null, "diagnostics": { "cacheHit": false, "source": "cold_start", "correlationId": "" } } } ``` ## Configuration `appsettings.json`: ```json { "FirstSignal": { "Cache": { "Backend": "inmemory", "TtlSeconds": 86400, "SlidingExpiration": true, "KeyPrefix": "orchestrator:first_signal:" }, "ColdPath": { "TimeoutMs": 3000 }, "SnapshotWriter": { "Enabled": false, "TenantId": null, "PollIntervalSeconds": 10, "MaxRunsPerTick": 50, "LookbackMinutes": 60 } }, "messaging": { "transport": "inmemory" } } ```