stage-auto-a pulse: respond config (threshold/quorum/local-only)
Persisted-key/value helpers for the respond-pipeline auto-fire gates: - respond_auto_threshold / set_… (Severity, default HIGH) - respond_require_quorum / set_… (bool, default True) - respond_local_only / set_… (bool, default False) Plus a _severity_rank helper for threshold comparison. Backing store is the existing pulse_settings table; this commit also adds generic pulse_setting_get / pulse_setting_set helpers in db.py so future pulse settings don't each need their own column-pair helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -313,6 +313,25 @@ def kill_switch_set(armed: bool, db_path: Path = DB_PATH) -> None:
|
|||||||
conn.execute(stmt)
|
conn.execute(stmt)
|
||||||
|
|
||||||
|
|
||||||
|
def pulse_setting_get(key: str, db_path: Path = DB_PATH) -> Optional[str]:
|
||||||
|
"""Read one pulse_settings row by key. None if unset."""
|
||||||
|
stmt = select(pulse_settings.c.value).where(pulse_settings.c.key == key)
|
||||||
|
with engine(db_path).connect() as conn:
|
||||||
|
row = conn.execute(stmt).fetchone()
|
||||||
|
return None if row is None else str(row.value)
|
||||||
|
|
||||||
|
|
||||||
|
def pulse_setting_set(key: str, value: str, db_path: Path = DB_PATH) -> None:
|
||||||
|
"""Upsert one pulse_settings row by key. Both args are strings."""
|
||||||
|
stmt = sqlite_insert(pulse_settings).values(key=key, value=value)
|
||||||
|
stmt = stmt.on_conflict_do_update(
|
||||||
|
index_elements=[pulse_settings.c.key],
|
||||||
|
set_=dict(value=stmt.excluded.value),
|
||||||
|
)
|
||||||
|
with engine(db_path).begin() as conn:
|
||||||
|
conn.execute(stmt)
|
||||||
|
|
||||||
|
|
||||||
# ---------- federation: peers + signal buffer ----------------------------
|
# ---------- federation: peers + signal buffer ----------------------------
|
||||||
|
|
||||||
def upsert_peer(row: dict, db_path: Path = DB_PATH) -> None:
|
def upsert_peer(row: dict, db_path: Path = DB_PATH) -> None:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from typing import Callable, Dict, List, Optional, Tuple
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from psyc import db, log
|
from psyc import db, log
|
||||||
|
from psyc.models import Severity
|
||||||
|
|
||||||
|
|
||||||
_log = log.get(__name__)
|
_log = log.get(__name__)
|
||||||
@@ -33,6 +34,73 @@ class PulseMode(str, Enum):
|
|||||||
AUTO_EXECUTE = "auto-execute"
|
AUTO_EXECUTE = "auto-execute"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- respond auto-fire gates -----------------------------------------
|
||||||
|
# Persisted as rows in pulse_settings (key/value pairs). All defaults are
|
||||||
|
# "safe" — quorum required, HIGH threshold, federation cases permitted only
|
||||||
|
# when quorum-met.
|
||||||
|
|
||||||
|
_KEY_RESPOND_THRESHOLD = "respond_auto_threshold"
|
||||||
|
_KEY_RESPOND_REQUIRE_QUORUM = "respond_require_quorum"
|
||||||
|
_KEY_RESPOND_LOCAL_ONLY = "respond_local_only"
|
||||||
|
|
||||||
|
_DEFAULT_THRESHOLD = Severity.HIGH
|
||||||
|
_DEFAULT_REQUIRE_QUORUM = True
|
||||||
|
_DEFAULT_LOCAL_ONLY = False
|
||||||
|
|
||||||
|
|
||||||
|
def _severity_rank(sev: Optional[Severity]) -> int:
|
||||||
|
"""Rank order for severity threshold comparison. Unknown / None → -1."""
|
||||||
|
if sev is None:
|
||||||
|
return -1
|
||||||
|
return {
|
||||||
|
Severity.LOW: 0,
|
||||||
|
Severity.MEDIUM: 1,
|
||||||
|
Severity.HIGH: 2,
|
||||||
|
Severity.CRITICAL: 3,
|
||||||
|
}.get(sev, -1)
|
||||||
|
|
||||||
|
|
||||||
|
def respond_auto_threshold() -> Severity:
|
||||||
|
raw = db.pulse_setting_get(_KEY_RESPOND_THRESHOLD)
|
||||||
|
if raw is None:
|
||||||
|
return _DEFAULT_THRESHOLD
|
||||||
|
try:
|
||||||
|
return Severity(raw)
|
||||||
|
except ValueError:
|
||||||
|
return _DEFAULT_THRESHOLD
|
||||||
|
|
||||||
|
|
||||||
|
def set_respond_auto_threshold(sev: Severity) -> None:
|
||||||
|
if not isinstance(sev, Severity):
|
||||||
|
raise ValueError(f"not a Severity: {sev!r}")
|
||||||
|
db.pulse_setting_set(_KEY_RESPOND_THRESHOLD, sev.value)
|
||||||
|
_log.info("pulse.respond.threshold.changed", severity=sev.value)
|
||||||
|
|
||||||
|
|
||||||
|
def respond_require_quorum() -> bool:
|
||||||
|
raw = db.pulse_setting_get(_KEY_RESPOND_REQUIRE_QUORUM)
|
||||||
|
if raw is None:
|
||||||
|
return _DEFAULT_REQUIRE_QUORUM
|
||||||
|
return raw == "1"
|
||||||
|
|
||||||
|
|
||||||
|
def set_respond_require_quorum(state: bool) -> None:
|
||||||
|
db.pulse_setting_set(_KEY_RESPOND_REQUIRE_QUORUM, "1" if state else "0")
|
||||||
|
_log.info("pulse.respond.quorum.changed", required=bool(state))
|
||||||
|
|
||||||
|
|
||||||
|
def respond_local_only() -> bool:
|
||||||
|
raw = db.pulse_setting_get(_KEY_RESPOND_LOCAL_ONLY)
|
||||||
|
if raw is None:
|
||||||
|
return _DEFAULT_LOCAL_ONLY
|
||||||
|
return raw == "1"
|
||||||
|
|
||||||
|
|
||||||
|
def set_respond_local_only(state: bool) -> None:
|
||||||
|
db.pulse_setting_set(_KEY_RESPOND_LOCAL_ONLY, "1" if state else "0")
|
||||||
|
_log.info("pulse.respond.local-only.changed", local_only=bool(state))
|
||||||
|
|
||||||
|
|
||||||
class Pipeline(BaseModel):
|
class Pipeline(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
title: str
|
title: str
|
||||||
|
|||||||
Reference in New Issue
Block a user