Time-to-First-Signal (TTFS) Architecture

Derived from Product Advisory (14-Dec-2025): UX and Time-to-Evidence Technical Reference; details the TTFS subsystem for providing immediate feedback on run/job status.

1) Overview

Time-to-First-Signal (TTFS) measures the latency from user action (opening a run, starting a scan, CLI invocation) to the first meaningful signal being displayed or logged. The design targets early feedback; the current source contracts and the remaining design proposals are identified below.

PCD-462 source review, 05a7dd3618 (2026-10-08). ReleaseOrchestrator owns the retained polled run-snapshot read. JobEngine publishes neither first-signal example previously shown here, and no first-signal SSE stream is published. The Web client deliberately reports streaming unavailable and uses polling. The Telemetry metrics library remains supported; its presence does not prove the proposed cache, streaming, latency or air-gap guarantees. Recheck FirstSignalEndpoints.cs, FirstSignalService.cs and first-signal.client.ts at their owning source paths in section 5 before treating this as a current contract. This review makes no deployment or live availability claim.

1.1 Design Goals

1.2 Signal Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                           TTFS Signal Flow                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   User Action          API Layer              Cache Layer      Data Layer   │
│   ───────────          ─────────              ───────────      ──────────   │
│                                                                             │
│   [Route Enter] ──┬──► /first-signal ───────► Valkey/Redis ─┐              │
│   [CLI Start]  ───┤        │                      │         │              │
│   [CI Job]     ───┘        │                      │         ▼              │
│                            │                      │    ┌──────────────┐    │
│                            ▼                      │    │ PostgreSQL   │    │
│                      ┌──────────┐                 │    │ first_signal │    │
│                      │ ETag     │◄────────────────┤    │ _snapshots   │    │
│                      │ Validation│                │    └──────────────┘    │
│                      └──────────┘                 │                        │
│                            │                      │                        │
│                            ▼                      ▼                        │
│                      ┌──────────────────────────────┐                      │
│                      │     Response Assembly        │                      │
│                      │ • kind (status indicator)    │                      │
│                      │ • phase (current stage)      │                      │
│                      │ • summary (human text)       │                      │
│                      │ • eta_seconds (estimate)     │                      │
│                      │ • last_known_outcome         │                      │
│                      │ • next_actions               │                      │
│                      └──────────────────────────────┘                      │
│                                    │                                        │
│                                    ▼                                        │
│                      ┌──────────────────────────────┐                      │
│                      │     SSE / Polling Client     │                      │
│                      └──────────────────────────────┘                      │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

2) Component Budgets

The 5-second P95 budget is allocated across components:

ComponentP50 BudgetP95 BudgetNotes
Frontend (skeleton + hydration)100ms150msNetwork-independent
Edge API (auth + routing)150ms250msJWT validation, rate limiting
Core Services (lookup + assembly)700ms1,500msCache hit vs cold path
SSE/WebSocket establishment—300msFallback to polling if exceeded
Total (warm path)700ms2,500msCache hit scenario
Total (cold path)1,200ms4,000msCache miss, compute required

3) Signal Kinds

The kind field indicates the current signal state:

KindDescriptionTypical DurationIcon
queuedJob waiting in queue0-30sQueue
startedJob has begun execution—Play
phaseJob in specific phaseVariesProgress
blockedWaiting on dependency/policy—Pause
failedJob has failed—Error
succeededJob completed successfully—Check
canceledJob was canceled—Cancel
unavailableSignal cannot be determined—Unknown

4) Signal Phases

The phase field indicates the current execution phase:

PhaseDescriptionSLO Target
resolveDependency/artifact resolutionP95 < 30s
fetchData retrieval (registry, advisories)P95 < 45s
restoreCache/snapshot restorationP95 < 10s
analyzeAnalysis execution (scan, policy)P95 < 120s
policyPolicy evaluationP95 < 15s
reportReport generation/uploadP95 < 30s
unknownPhase cannot be determined—

5) Current API boundary and withdrawn examples

5.1 Retained polled run snapshot

GET /api/v1/release-orchestrator/runs/{runId:guid}/first-signal requires orch:read and the host’s required tenant context. Its current TenantResolver also requires X-Tenant-Id and uses that value for the snapshot lookup; this source review does not establish header-to-claim binding or tenant-isolation acceptance. A run GUID is not a JobEngine job identifier.

ContractCurrent source behavior
BodyFirstSignalResponse: runId, nullable firstSignal, summaryEtag. The nested signal has type, stage, step, message, at, artifact and lastKnownOutcome; the former flat kind/phase/ETA body is not this response.
Existing valid snapshot200 with the mapped response. The service retrieves stored data; it does not compute a cold-start signal from a job.
Matching If-None-Match304 without a response body.
Missing or malformed snapshot404; malformed stored signal JSON is deleted by the service before that result.
HeadersETag, Cache-Control: private, max-age=60, Cache-Status and X-FirstSignal-Source. The current snapshot-only service reports Cache-Status: miss and source snapshot when data exists; it does not use an L1/L2 cache.

The handler also maps a provider NotAvailable result to 204 and argument/tenant resolution failures to 400. That status mapping is not evidence that an alternate provider or a cold-computation branch is composed.

Source authority:

The host registers persistence and maps this endpoint explicitly. Its snapshot repository filters release_orchestrator.first_signal_snapshots by tenant and run. The Web client polls native run GUIDs through this owner route; its separate Console-generated run:: identifiers use the Console snapshot contract.

5.2 Historical 2025-12-14 SSE proposal - unavailable

The following was a design example, not a retained JobEngine contract. No first-signal SSE handler is composed. FirstSignalHttpClient.streamFirstSignal fails immediately without constructing an EventSource, so its store uses polling. Changing the prefix or replacing a job ID with a run ID does not create a stream.

GET /api/v1/jobengine/stream/jobs/{jobId}/first-signal
Accept: text/event-stream

event: signal
data: {"kind":"started","phase":"analyze",...}

event: signal
data: {"kind":"phase","phase":"policy",...}

event: done
data: {"kind":"succeeded",...}

5.3 Historical 2025-12-14 CLI interaction proposal

This interaction sketch is retained as design history, not a verified CLI command or proof that a first-signal stream exists.

# Job status with immediate signal
stella job status <job-id> --watch

# Output progression:
# [queued] Waiting in queue (position: 3)
# [started] Job started on worker-7
# [phase:analyze] Scanning image layers (47%)
# [succeeded] Completed in 2m 34s

6) Caching Strategy

The retained read is snapshot-only in ReleaseOrchestrator’s own database. The multi-tier and air-gap streaming design below remains a proposal; it does not describe the composed first-signal service or establish a latency guarantee.

6.1 Cache Tiers

TierStorageTTLUse Case
L1In-memory (per-instance)1sHot path, same-instance requests
L2Valkey/Redis5sCross-instance, active jobs
L3PostgreSQL24hPersistent snapshots, air-gap mode

6.2 Cache Keys

ttfs:job:{tenant_id}:{job_id}:signal      # Current signal
ttfs:job:{tenant_id}:{job_id}:eta         # ETA prediction
ttfs:run:{tenant_id}:{run_id}:signals     # Run-level aggregation
ttfs:tenant:{tenant_id}:failure_sig       # Failure signatures

6.3 Air-Gap Mode

The historical streaming design proposed the following for air-gapped environments:

  1. PostgreSQL NOTIFY/LISTEN replaces pub/sub for real-time updates
  2. Polling fallback with 2-second intervals
  3. first_signal_snapshots table serves as L2 cache
  4. SSE degradation to long-polling; no such first-signal server fallback is composed today

7) Telemetry & Observability

7.1 Metrics

MetricTypeDescription
ttfs_latency_secondsHistogramEnd-to-end signal latency
ttfs_cache_latency_secondsHistogramCache lookup time
ttfs_cold_latency_secondsHistogramCold path computation time
ttfs_signal_totalCounterSignals by kind/surface
ttfs_cache_hit_totalCounterCache hits
ttfs_cache_miss_totalCounterCache misses
ttfs_slo_breach_totalCounterSLO breaches
ttfs_error_totalCounterErrors by type

7.2 Labels

TimeToFirstSignalMetrics.CreateSignalTags emits these common labels:

signal_source and tenant_id are added only when the caller supplies nonblank values. Error measurements additionally carry error_type and optionally error_code; direct SLO breach measurements carry actual/target seconds.

7.3 SLO Definitions

# Prometheus recording rules
- record: ttfs:slo:p50_target
  expr: 2.0  # seconds

- record: ttfs:slo:p95_target
  expr: 5.0  # seconds

- record: ttfs:slo:compliance
  expr: |
    histogram_quantile(0.95, sum(rate(ttfs_latency_seconds_bucket[5m])) by (le))
    < 5.0

# Alerting rules
- alert: TtfsSloBreachP95
  expr: histogram_quantile(0.95, sum(rate(ttfs_latency_seconds_bucket[5m])) by (le)) > 5.0
  for: 5m
  labels:
    severity: page
  annotations:
    summary: "TTFS P95 exceeds 5s SLO"

- alert: TtfsHighErrorRate
  expr: rate(ttfs_error_total[5m]) > 0.1
  for: 2m
  labels:
    severity: warning

8) Frontend Integration

8.1 Component Hierarchy

FirstSignalCard (Smart Component)
├── FirstSignalStore (Signal-based State)
│   ├── SSE subscription
│   ├── Polling fallback
│   └── ETag caching
├── StatusIndicator (Dumb Component)
│   └── kind → icon + color mapping
├── PhaseProgress (Dumb Component)
│   └── phase → progress bar
└── ActionButtons (Dumb Component)
    └── next_actions rendering

8.2 State Machine

type FirstSignalLoadState = 'idle' | 'loading' | 'streaming' | 'error' | 'done';

// State transitions:
// idle → loading (initial fetch)
// loading → streaming (SSE connected) | error (fetch failed)
// streaming → done (terminal signal) | error (connection lost)
// error → loading (retry)

8.3 Animation Tokens

TokenValueUsage
--motion-duration-quick150msSkeleton fade, icon transitions
--motion-duration-normal250msCard expansion, phase transitions
--motion-duration-slow400msSuccess/failure celebrations
--motion-easing-standardcubic-bezier(0.4, 0, 0.2, 1)Default easing
--motion-easing-deceleratecubic-bezier(0, 0, 0.2, 1)Entries
--motion-easing-acceleratecubic-bezier(0.4, 0, 1, 1)Exits

8.4 Browser TTFS Emission

9) Failure Signatures

Failure signatures enable predictive “last known outcome” by pattern-matching historical failures.

9.1 Signature Schema

{
  "signature_hash": "sha256:abc123...",
  "pattern": {
    "phase": "analyze",
    "error_code": "LAYER_EXTRACT_FAILED",
    "image_pattern": "registry.io/.*:v1.*"
  },
  "outcome": {
    "likely_cause": "Registry rate limiting",
    "mttr_p50_seconds": 300,
    "suggested_action": "Wait 5 minutes and retry"
  },
  "confidence": 0.87,
  "sample_count": 42
}

9.2 Usage

When a job enters a known failure pattern:

  1. Match current job state against failure_signatures table
  2. Enrich signal with last_known_outcome.likely_cause
  3. Predict ETA based on historical MTTR
  4. Suggest remediation via next_actions

10) Database Schema

The retained first-signal snapshot repository uses release_orchestrator.first_signal_snapshots in ReleaseOrchestrator’s own database. The predecessor docs/db/schemas/ttfs.sql design is not a current startup migration authority or a cross-service read contract.

10.1 Core Tables

TablePurpose
release_orchestrator.first_signal_snapshotsRetained snapshot state, selected by tenant and run GUID
scheduler.failure_signaturesJobEngine-owned failure patterns; the retained snapshot reader does not join this table

The former scheduler.first_signal_snapshots and scheduler.ttfs_events table names belong to the historical design and are not this reader’s storage contract.

10.2 Hourly Rollup View

The historical design proposed scheduler.ttfs_hourly_summary; it is not a dependency or a measured guarantee of the retained snapshot read.

11) Testing Requirements

11.1 Unit Tests

11.2 Integration Tests

11.3 Deterministic Fixtures

// tests/fixtures/ttfs/
export const TTFS_FIXTURES = {
  FROZEN_TIMESTAMP: '2025-12-04T12:00:00.000Z',
  DETERMINISTIC_SEED: 0x5EED2025,
  SAMPLE_JOB_ID: '550e8400-e29b-41d4-a716-446655440000',
  SAMPLE_TENANT_ID: 'tenant-test-001'
};

12) Observability

12.1 Grafana Dashboard

The TTFS observability dashboard provides real-time visibility into signal latency, cache performance, and SLO compliance.

Key panels:

12.2 Alert Rules

TTFS alerts are defined in docs/modules/telemetry/operations/alerts/ttfs-alerts.yaml.

Critical alerts:

AlertThresholdFor
TtfsP95HighP95 > 5s5m
TtfsSloBreach>10 breaches in 5m1m
FirstSignalEndpointDownOrchestrator unavailable2m

Warning alerts:

AlertThresholdFor
TtfsCacheHitRateLow<70%10m
TtfsErrorRateHigh>1%5m
FirstSignalEndpointLatencyHighP95 > 500ms5m

12.3 Load Testing

Load tests validate TTFS performance under realistic conditions.

Scenarios:

Thresholds:

13) References