- a5/rq_heartbeat.py: HeartbeatStore + Heartbeat.run_tick (thin scheduler/resume layer) - a5/rq_heartbeat_cli.py: status/tick/enable/disable/resume/approve/deny/priority - a5/test_a5.py: 31 deterministic tests (tick-lock incl. ownership, eligibility, kill-switch, approval, circuit, git-conflict, bounded single A4) - a5/DESIGN.md, a5/README.md, a5/scripts/a5_heartbeat_tick.sh - a2/rq_mission.py: add read-only mission_list() for A5 enumeration (additive) - Fresh checker: PASS after tick-lock ownership repair (Regressionschutz lock_owned)
145 lines
6.1 KiB
Markdown
145 lines
6.1 KiB
Markdown
# Red Queen — A5: Controlled Heartbeat & Resume (V1)
|
|
|
|
Der **Controlled Heartbeat & Resume** verbindet den A4 **Bounded Orchestrator** mit
|
|
einem **dünnen Scheduling-/Resume-Layer** (Heartbeat), sodass eine längere Mission
|
|
über **mehrere getrennte, sichere, restart-feste Arbeitszyklen** fortgesetzt werden
|
|
kann — **ohne unkontrollierten Dauer-LLM-Loop**.
|
|
|
|
Der Heartbeat ist **NICHT der Orchestrator** und **keine neue Mission-Engine**:
|
|
|
|
- **A2** (`missions.db`) bleibt autoritativ für Mission-/WP-State.
|
|
- **A3** (`safety.db`) bleibt verbindliche Safety-Decision (Circuit, Evaluate, Redact).
|
|
- **A4** bleibt die Orchestrierung (`run_bounded` = garantiert endender Zyklus).
|
|
- **A5** ist eine **reine, inaktive Scheduling-/Resume-Schicht**, die pro Tick
|
|
**GENAU EINEN** bounded A4-Zyklus anstößt, das Ergebnis persistiert und sofort
|
|
endet. **Kein while-True, kein Daemon, kein self-reschedule.**
|
|
|
|
```
|
|
SCHEDULER (Hermes Cron) → Heartbeat.run_tick()
|
|
→ Run-ID (Idempotenz) → Kill-Switch-Gate → Tick-Lock
|
|
→ Mission-Eligibility (inkl. Circuit + Resume-Pending)
|
|
→ A3 check_safety_state → optional Git-Preflight → Approval-Gate
|
|
→ GENAU EIN A4 run_bounded(max_steps=1)
|
|
→ Ergebnis persistieren → Lock freigeben → EXIT
|
|
```
|
|
|
|
---
|
|
|
|
## Integrations (verified from source)
|
|
|
|
- **A2 `MissionStore`** (`a2/rq_mission.py`, `missions.db`):
|
|
- `mission_list()` [NEW, read-only, additiv — A5-Enumeration]
|
|
- `mission_read(id)`, `mission_state(id)`, `wp_state(id)`
|
|
- Transitionen: `mission_transition`, `mission_pause`, `mission_resume`,
|
|
`mission_complete`, `wp_transition`, `wp_complete`.
|
|
- **A3 `SafetyStore`** (`a3/rq_safety.py`, `safety_db`):
|
|
- `check_safety_state(mid, wp)` — fail-closed `STATE_INCONSISTENT`
|
|
- `evaluate_next_action(mid, wp, is_mutating, persist)`, `circuit_state`,
|
|
`open_circuit`, `attempts`, `safety_evidence`, `redact_secret`.
|
|
- **A4 `Orchestrator`** (`a4/rq_orchestrator.py`):
|
|
- `run_bounded(mission_id, max_steps=1, actor)` — EXACTLY-N bounded steps,
|
|
garantiertes Ende. **Heartbeat ruft DAS GENAU EINMAL pro Tick.**
|
|
- **Hermes native Cron**: `no_agent`, `executions.db`, `.tick.lock`, `context_from`,
|
|
`workdir`, `delivery` — die bevorzugte Scheduling-Plattform (kein zweiter Scheduler).
|
|
|
|
---
|
|
|
|
## Eigene Registry (A5-spezifisch, `heartbeat.db`)
|
|
|
|
A5 führt **keine neue Missions-/Safety-DB**, sondern nur eine schlanke Registry für
|
|
Heartbeat-eigene Steuerung:
|
|
|
|
- `kill_switch` — `AUTONOMOUS_EXECUTION_ENABLED`, **Default OFF**. OFF → `run_tick`
|
|
liefert `HB_KILL_SWITCH_OFF` vor jeder A4-Mutation. `status`/`health` bleiben read-only.
|
|
- `lock` — **EIN-Tick-Lock** (idempotent). `acquire_lock` → `ok|locked|recover`.
|
|
Stale nach `TICK_LOCK_STALE_SECONDS` (600s).
|
|
- `run_id`-Ledger + `tick_log` — jede Tick-Ausführung wird persistiert (Idempotenz).
|
|
- `approval_gates`, `resume_pending`, `notification_dedup`, `priority`.
|
|
|
|
**Restart-Festigkeit:** Alle DBs (missions, safety, heartbeat) liegen auf **persistenter
|
|
Volume**, nicht process-local. Ein frischer Prozess liest den State neu; neue Run-ID,
|
|
keine Doppel-/Neu-Ausführung, kein verlorener Pending-State.
|
|
|
|
---
|
|
|
|
## A5-Result-Codes (§10)
|
|
|
|
```
|
|
HB_NO_ACTIVE_MISSION, HB_MISSION_NOT_ELIGIBLE, HB_SAFETY_BLOCKED,
|
|
HB_STATE_CONFLICT, HB_NO_RUNNABLE_WORK, HB_BOUNDED_RUN_COMPLETE,
|
|
HB_MISSION_COMPLETED, HB_APPROVAL_REQUIRED, HB_ESCALATED, HB_CIRCUIT_OPEN,
|
|
HB_TIMEOUT, HB_INTERNAL_ERROR,
|
|
HB_LOCKED, HB_KILL_SWITCH_OFF, HB_PAUSED, HB_CANCELLED,
|
|
HB_GIT_CONFLICT, HB_RECOVERY_REQUIRED
|
|
```
|
|
|
|
`health()`/`status` sind read-only und immer erlaubt. Jeder gescheiterte Gate liefert
|
|
einen **stabilen HB_*-Code** (fail-closed, keine partielle Mutation).
|
|
|
|
---
|
|
|
|
## CLI
|
|
|
|
`python3 a5/rq_heartbeat_cli.py` — deterministisch, kein Loop, kein Root:
|
|
|
|
```
|
|
status Kill-Switch, letzter Tick, Registry-Infos
|
|
tick [--mission ID] EIN bounded Heartbeat-Zyklus (ohne --execute read-only)
|
|
enable Kill-Switch ON (bewusst; erlaubt A4-Mutation)
|
|
disable Kill-Switch OFF (Fail-closed)
|
|
resume --mission ID PAUSED-Mission explizit als resume-fähig markieren
|
|
approve --gate ID Approval-Gate bewilligen
|
|
deny --gate ID Approval-Gate ablehnen
|
|
priority --mission --prio N
|
|
```
|
|
`--execute` ist ein **globales** Flag (vor dem Subcommand): ohne ist `tick` read-only,
|
|
mit injiziert es einen echten `Orchestrator` und erlaubt einen A4-Mutation.
|
|
|
|
---
|
|
|
|
## Cron-Integration (Hermes native)
|
|
|
|
```yaml
|
|
cron:
|
|
# One-Shot oder wiederkehrend, no_agent=True, Script aus $HERMES_HOME/scripts
|
|
script: a5_heartbeat_tick.sh # ruft EINEN bounded Tick; kein Loop
|
|
```
|
|
|
|
One-Shot-Jobs werden nach Ausführung automatisch entfernt → es bleibt **kein
|
|
unkontrollierter Dauer-Job** zurück (`hermes cron list` = 0). Der Tick läuft mit
|
|
`no_agent` über die Heartbeat-Library, **ohne** eigenständigen LLM-Loop.
|
|
|
|
---
|
|
|
|
## Test Harness
|
|
|
|
`python3 a5/test_a5.py` — isolierte, deterministische Tests (temp-DBs, kein Netz,
|
|
kein echter A4-Dispatch außer einem bounded real-Orchestrator-Test). Exit-Code 0 = PASS.
|
|
Deckt: Kill-Switch off/on, **Tick-Lock (overlap → `HB_LOCKED` und Lock-Erhalt)**,
|
|
Lock-Release nach Owner-Tick, Eligibility (cancelled/paused/resumable), Priorität,
|
|
**GENAU EIN** bounded A4-Call, No-Runnable, Approval-Block/Grant/Resume,
|
|
Circuit-Block, Git-Konflikt, unbekannte Mission, read-only health,
|
|
bounded-real-Orchestrator-Contract.
|
|
|
|
Regressions: `a2/test_a2.py` (61), `a3/test_a3.py` (69), `a4/test_a4.py` (83).
|
|
|
|
---
|
|
|
|
## Fail-closed & Approval
|
|
|
|
- `mission_completion_gate(..., final_review_pass=False)` — A5 übergibt standardmäßig
|
|
`False` → **kein autonomes Final-Review/Auto-Complete in V1**.
|
|
- Approval-Gates persistieren in der Registry; erst nach `approve` wird die Mission
|
|
wieder runnable.
|
|
- `open_circuit` (A3) ist **keyword-only** nach scope_type/scope_id:
|
|
`open_circuit("MISSION", mid, trigger=..., reason=...)`.
|
|
|
|
---
|
|
|
|
## Known Limitations
|
|
|
|
- A5 selbst startet **keine** echte A4-Mutation außerhalb eines ausdrücklichen
|
|
`--execute`-Ticks. Produktiver Heartbeat/Cron ist erst nach expliziter Freigabe
|
|
(Kill-Switch) aktiv; Default OFF.
|
|
- Kein automatisches Rain; kein Self-Improvement-Automation; kein unkontrollierter
|
|
Dauerlauf.
|