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 intoMay 22, 2026
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Probe-wide invariants
snapshot rather than raising
test pins `httpx.AsyncClient.get/.head` never awaited
even on transport errors / non-200 responses
siblings; runner catches in defense-in-depth wrapper
§4 PM-open questions
TS contract drift (additive — ST2 will reconcile)
The ST1 internal `ServiceHealthSnapshot` is richer than CC#2's
`HeartbeatService` interface (PR #103):
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
degraded threshold; non-200 unhealthy with redacted error;
token-in-transport-error redacted; cross-probe "0 calls when
all envs missing" invariant)
defensive copy; scheduled wrapper swallows errors + logs;
shutdown clears cache; interval env reading; registry-side
wire-in)
Cascade
Base: `feature/phase2-upgrades`. ST2 stacks on this.
🤖 Generated with Claude Code