Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-FEAT-HEARTBEAT ST1 — 5 service probes + scheduler integration - #116

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FEAT-HEARTBEAT-ST1
May 22, 2026
Merged

feat(kora): KR-FEAT-HEARTBEAT ST1 — 5 service probes + scheduler integration#116
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FEAT-HEARTBEAT-ST1

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Phase 2 Feature 2 backend. Lands the probe library + heartbeat-
scheduler integration. ST2 flips `/api/heartbeat/services` from
stub to live snapshot-cache read.

Bucket spec: `17_cc_bucket_prompts/KR-FEAT-HEARTBEAT_backend_service_probe.md`

Module layout

Module Lines Purpose
`heartbeat_probes/types.py` 53 Pydantic snapshot + 4-value status literal
`heartbeat_probes/base.py` 226 ServiceProbe Protocol + helpers (resolve_env, sanitize_error, snapshot constructors, with_timeout)
`heartbeat_probes/{vercel,sentry,doppler,supabase,fly}.py` 549 total One probe per service
`heartbeat_probes/runner.py` 149 default_probes / run_all_probes / current_service_snapshots / scheduler-facing wrapper
`listeners/heartbeat_probes_listener.py` 127 Daemon listener + heartbeat-task registration

Probe-wide invariants

  • 10s per-probe timeout via `with_timeout` — returns timeout-
    snapshot rather than raising
  • Missing auth env → status=unknown + ZERO outbound call
    test pins `httpx.AsyncClient.get/.head` never awaited
  • Empty/whitespace env values treated as unset (Doppler corner)
  • All error strings sanitized — never include the auth token,
    even on transport errors / non-200 responses
  • Per-probe failure isolated — one probe raising doesn't block
    siblings; runner catches in defense-in-depth wrapper

§4 PM-open questions

Q Resolution
Q1 cadence (5min, same as MCP — SEPARATE task) 300s default; "heartbeat.service_probes" registered as DISTINCT task name from "mcp.health_check" so one slow cycle doesn't block the other. Pinned by test.
Q2 cache_warming flag Deferred to ST2
Q3 Doppler service-token doc Deferred to ST2

TS contract drift (additive — ST2 will reconcile)

The ST1 internal `ServiceHealthSnapshot` is richer than CC#2's
`HeartbeatService` interface (PR #103):

  • status enum: 4 values (adds `unknown`)
  • error: `string | null` (new)
  • latency_ms: `number | null` (was non-nullable)

ST2 endpoint projection extends the TS interface additively —
same pattern as KR-MCP-CONSUMPTION ST2 / KR-MCP-CLIENTS-FLIP.
Not breaking — existing consumers see the new fields as optional.

Test plan

  • 59 tests in `tests/kora_cli/test_heartbeat_probes/`
  • 17 base / type / helper tests
  • 21 per-probe tests (missing env zero-call; 200 healthy;
    degraded threshold; non-200 unhealthy with redacted error;
    token-in-transport-error redacted; cross-probe "0 calls when
    all envs missing" invariant)
  • 21 runner + listener tests (snapshot cache; failure isolation;
    defensive copy; scheduled wrapper swallows errors + logs;
    shutdown clears cache; interval env reading; registry-side
    wire-in)
  • 243/243 cross-bucket regression
  • Ruff clean

Cascade

Base: `feature/phase2-upgrades`. ST2 stacks on this.

🤖 Generated with Claude Code

…gration

Phase 2 Feature 2 backend. Lands the probe library + heartbeat-
scheduler integration; ST2 flips the panel endpoint from stub to
live snapshot read.

# Module layout

`kora_cli/heartbeat_probes/`:
  - `types.py`       — Pydantic ServiceHealthSnapshot + 4-value
                       ServiceStatus literal (extends TS contract's
                       3-value with "unknown" — see note below)
  - `base.py`        — ServiceProbe Protocol + helpers:
                         resolve_env (empty/whitespace → unset)
                         sanitize_error (token redaction)
                         snapshot_for_auth_missing / _timeout /
                           _unexpected_error
                         with_timeout (10s ceiling, returns
                           timeout-snapshot rather than raising)
  - `vercel.py`      — VercelProbe (deployments-last-24h +
                       error_rate; degraded > 10%)
  - `sentry.py`      — SentryProbe (unresolved issues; degraded > 10)
  - `doppler.py`     — DopplerProbe (workplace endpoint reachability;
                       projects_total / oldest_secret_age = "unknown"
                       until per-project queries land)
  - `supabase.py`    — SupabaseProbe (PostgREST HEAD; healthy on
                       200/204; connections_pct = "unknown" until
                       management-API auth wired)
  - `fly.py`         — FlyProbe (prod kora-runtime + optional
                       staging app; degraded when machines partial)
  - `runner.py`      — default_probes / run_all_probes /
                       current_service_snapshots (defensive copy) +
                       run_all_probes_scheduled wrapper

`kora_cli/listeners/heartbeat_probes_listener.py`:
  - HeartbeatProbesListener — startup logs interval; shutdown
    clears snapshot cache (no stale data across daemon restart)
  - register_daemon_listener("heartbeat_probes", _factory)
  - register_periodic_task("heartbeat.service_probes",
    interval_seconds=_read_probe_interval(),
    callable=run_all_probes_scheduled)
  - KORA_HEARTBEAT_PROBE_INTERVAL_SEC env override (default 300s
    per §4 Q1 ruling)

`kora_cli/listeners/__init__.py`:
  - Wire-in for the new listener after mcp_consumption

# Probe-wide rules enforced

  - 10-second per-probe timeout via `with_timeout`
  - Missing auth env → status="unknown" + ZERO outbound call
    (test pins `httpx.AsyncClient.get/.head` never awaited)
  - Empty/whitespace env values treated as unset (Doppler corner)
  - All error strings sanitized — never include the auth token
    (test pins for httpx error message + non-200 response paths)
  - Per-probe failure isolated — one probe raising doesn't block
    siblings (runner catches in defense-in-depth wrapper)

# §4 PM-open questions

Filing in PR body for ruling before ST2:

  - Q1 cadence default 5min (same as MCP health, SEPARATE task):
    chose 300s default per spec. Both registered as DISTINCT
    periodic-task names ("heartbeat.service_probes" vs
    "mcp.health_check") so one slow cycle doesn't block the
    other. Pinned by test_mcp_and_heartbeat_tasks_are_independent.
  - Q2 cache_warming flag: deferred to ST2 endpoint flip.
  - Q3 Doppler service token doc: deferred to ST2.

# TS contract drift (additive)

The ST1 internal `ServiceHealthSnapshot` is richer than CC#2's
HeartbeatService TS interface (PR #103):
  - status enum: 4 values (adds "unknown")
  - error: str | None (new field)
  - latency_ms: int | None (was non-nullable)

ST2 endpoint projection will additively extend the TS interface
(same pattern as KR-MCP-CONSUMPTION ST2 / KR-MCP-CLIENTS-FLIP).
Not breaking — existing consumers see the new fields as optional.

# Tests

59 tests in tests/kora_cli/test_heartbeat_probes/:
  - test_base.py — 17 tests (status enum, snapshot extra=forbid,
    resolve_env edge cases, sanitize_error redaction, auth_missing/
    timeout/unexpected snapshot constructors, with_timeout wrapper)
  - test_probes.py — 21 tests (per-probe: missing env zero-call;
    200 healthy; degraded threshold; non-200 unhealthy with
    redacted error; token-in-transport-error-message redacted;
    cross-probe "0 calls when all envs missing" invariant)
  - test_runner_and_listener.py — 21 tests (default_probes shape;
    snapshot cache populate; failure isolation; defensive copy;
    scheduled wrapper swallows errors + logs; listener startup
    no-op + shutdown clears cache; interval env reading; daemon
    + periodic registry both populated at import time)

59/59 pass; 243/243 cross-bucket regression
(test_listeners/ + test_daemon.py + tests/kora_mcp/ +
tests/kora_cli/test_heartbeat_probes/) + 1 expected skip
(integration test gated). Ruff clean.

# LOC summary

  - prod:  1,099 LOC across 9 files (probes + runner + listener)
  - tests:   980 LOC across 4 files
  - bucket estimated ~600 prod + ~500 test; over due to one-file-
    per-probe layout + comprehensive token-redaction tests at
    every probe boundary

# After ST2

Endpoint flips from stub to snapshot-cache read. Doppler env-
mapping doc updated with Phase 2 Feature 2 token rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker
rafe-walker merged commit 24aaa44 into feature/phase2-upgrades May 22, 2026
@rafe-walker
rafe-walker deleted the feat/kora-KR-FEAT-HEARTBEAT-ST1 branch May 22, 2026 16:11
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant