feat(railiance): adopt state-hub edge relay beachhead for WP-0015
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m12s

Deploy actcore-statehub-edge-relay, point STATE_HUB_URL at it, retire the
bespoke state-hub bridge, and accept edge-relay queued write receipts in
report and ops evidence sinks.
This commit is contained in:
tegwick 2026-07-09 01:04:24 +02:00
parent 1a0636845a
commit cdbe4de2bf
13 changed files with 256 additions and 136 deletions

View file

@ -18,8 +18,10 @@ Supported queries:
- phase5_stabilization_check: hub-visible Phase 5 stabilization gates
- legacy_meter_weekly_review: GET {STATE_HUB_URL}/legacy-meter/weekly-review
No caching state hub data is live operational state and must not be stale
within a single workflow run.
When STATE_HUB_URL points at the state-hub edge relay, allowlisted GET reads may
be served from a stale local cache during upstream outages (`X-StateHub-Edge-Cache:
stale`). activity-core treats those as ordinary successful reads so workflows can
continue with last-known hub state.
Config: STATE_HUB_URL env var (default: http://127.0.0.1:8000).
"""

View file

@ -10,7 +10,11 @@ from typing import Any
import httpx
from activity_core.context_resolvers.ops_inventory import _sanitize_url
from activity_core.state_hub_write import apply_progress_scope_fields, idempotency_headers
from activity_core.state_hub_write import (
apply_progress_scope_fields,
idempotency_headers,
parse_state_hub_write_response,
)
_DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
_INTER_HUB_SINK_TYPES = {
@ -161,8 +165,16 @@ def _post_state_hub_progress(
headers=idempotency_headers(run_id, context_key, event_type),
timeout=float(sink.get("timeout_seconds", 10.0)),
)
resp.raise_for_status()
data = resp.json()
data = parse_state_hub_write_response(resp)
if data.get("queued"):
return {
"type": "state-hub-progress",
"status": "queued",
"event_type": event_type,
"outbox_id": data.get("outbox_id"),
"idempotency_key": data.get("idempotency_key") or idempotency_key,
"context_key": context_key,
}
return {
"type": "state-hub-progress",
"status": "posted",

View file

@ -16,7 +16,11 @@ from activity_core.runtime_paths import (
custodian_repo_root,
resolve_runtime_path,
)
from activity_core.state_hub_write import apply_progress_scope_fields, idempotency_headers
from activity_core.state_hub_write import (
apply_progress_scope_fields,
idempotency_headers,
parse_state_hub_write_response,
)
_DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
@ -154,8 +158,15 @@ def _post_state_hub_progress(
headers=idempotency_headers(run_id, instruction_id, event_type),
timeout=float(sink.get("timeout_seconds", 10.0)),
)
resp.raise_for_status()
data = resp.json()
data = parse_state_hub_write_response(resp)
if data.get("queued"):
return {
"type": "state-hub-progress",
"status": "queued",
"event_type": event_type,
"outbox_id": data.get("outbox_id"),
"idempotency_key": data.get("idempotency_key"),
}
return {
"type": "state-hub-progress",
"status": "posted",

View file

@ -24,7 +24,7 @@ from uuid import UUID
import httpx
from activity_core.schedule_manager import schedule_id
from activity_core.state_hub_write import idempotency_headers
from activity_core.state_hub_write import idempotency_headers, parse_state_hub_write_response
_DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
@ -187,8 +187,14 @@ def post_missed_fire_alert(
headers=idempotency_headers("schedule_miss", health.activity_id, last_fired),
timeout=timeout_seconds,
)
resp.raise_for_status()
data = resp.json()
data = parse_state_hub_write_response(resp)
if data.get("queued"):
return {
"type": "schedule-miss-alert",
"status": "queued",
"outbox_id": data.get("outbox_id"),
"idempotency_key": data.get("idempotency_key"),
}
return {
"type": "schedule-miss-alert",
"status": "posted",

View file

@ -8,15 +8,15 @@ write's identity. The guarantee lives on the write itself and does **not** depen
on a live dedup read, so it holds even when the beachhead is serving offline.
activity-core does not implement the queue/cache (that is state-hub's beachhead);
it only emits the key so the beachhead / State Hub can dedup on flush. The header
passes untouched through the existing ``actcore-state-hub-bridge`` proxy and is
ignored by State Hub versions that do not yet honour it.
it only emits the key so the beachhead / State Hub can dedup on flush.
"""
from __future__ import annotations
from typing import Any
import httpx
IDEMPOTENCY_HEADER = "Idempotency-Key"
@ -48,3 +48,13 @@ def idempotency_key(*parts: str | None) -> str:
def idempotency_headers(*parts: str | None) -> dict[str, str]:
"""Return the header dict to attach to a State Hub write."""
return {IDEMPOTENCY_HEADER: idempotency_key(*parts)}
def parse_state_hub_write_response(resp: httpx.Response) -> dict[str, Any]:
"""Normalize a State Hub write response, including edge-relay queued receipts."""
if resp.status_code == 202:
data = resp.json()
if data.get("queued"):
return data
resp.raise_for_status()
return resp.json()