From d5c8656f24d6e726f6c9b3a2f098e28cd39bbbac Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Fri, 14 Aug 2026 19:34:30 -0500 Subject: [PATCH] feat(api): mirror the last control outcome into the enriched feed (#346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pithead's host runner caps its synchronous /status poll after a worker-apply at 20s, but control-apply's auto-rollback can take minutes — the change-history row froze at 'accepted' with no way to catch up short of a new authenticated dial to the rig's control port (a token the dashboard container never holds). The rig now mirrors its own last outcome into the already-open read feed: _api_control_json reads {change_id, status, reason} from the status.json _control_status already writes 0644, and _api_rigforge_block serves it as rigforge.control. Missing, unreadable, or malformed status.json degrades to control: null — the mirror can never break the feed — and pithead's parse_worker_control_status filters non-terminal records itself. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 10 ++++++++++ docs/operations.md | 5 +++-- docs/pithead-integration.md | 5 ++++- rigforge.sh | 11 ++++++++++- tests/run.sh | 14 ++++++++++++++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e045bf9..9c331de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ All notable changes to RigForge are documented here. The format is based on ## [Unreleased] +### Added + +- **The enriched feed mirrors the last control outcome (#346).** Pithead's host runner caps its + synchronous `/status` poll after a worker-apply, so a slow auto-rollback outruns it and the change + history row froze at `accepted`. The sister API's `rigforge` block now carries `control` — + `{change_id, status, reason}` read from the control status file — so the next routine poll of the + open read feed catches the terminal outcome without a new authenticated dial to the control port. + A rig that never took a control change, or an unreadable/malformed status file, serves + `control: null`; the mirror never breaks the feed. + ## [1.14.0] - 2026-08-02 ### Added diff --git a/docs/operations.md b/docs/operations.md index d02978d..e3a661a 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -534,8 +534,9 @@ second read-only HTTP endpoint (default `:8081`, keys `api_port`/`api_bind`) wit `:8080` XMRig API has **plus** the data only RigForge knows: applied tune knobs and the last tune run (`/tune`), hashrate-per-watt from RAPL, the doctor probes — HugePages, MSR state, governor, RAM channels/speeds, memory-profile and SMT state, throttling — the watchdog's armed/thermal state, -the effective **writable** config (`config`, pool secrets masked; #253), and a config revision + -last-change provenance (`config_meta`; #254) — as JSON (`/health`, or nested under +the effective **writable** config (`config`, pool secrets masked; #253), a config revision + +last-change provenance (`config_meta`; #254), and the last control-path outcome (`control`, +`{change_id, status, reason}` or `null`; #346) — as JSON (`/health`, or nested under `rigforge` in `/1/summary` and `/2/summary`). It follows XMRig's own architecture: one tiny persistent server (python3 stdlib, ~10 MB idle) ships pre-computed bytes, so a request costs microseconds and cannot touch mining performance; a systemd timer recomputes the state every 15 diff --git a/docs/pithead-integration.md b/docs/pithead-integration.md index 68e22dd..dfb7ad1 100644 --- a/docs/pithead-integration.md +++ b/docs/pithead-integration.md @@ -82,7 +82,10 @@ pithead#235): config — exactly the control-path allowlist, pool secrets masked; see the prefill note in §3), and `config_meta` (`{revision, changed_at, source, last_change_id}` — `revision` is a content hash of the writable config that changes iff that config changes, so a poller can detect a change made directly - on the rig; `source` is `control`/`local`/`restore`; see §3). + on the rig; `source` is `control`/`local`/`restore`; see §3), and `control` (the last control-path + outcome, `{change_id, status, reason}` mirrored from the control status file, so a poller that + missed a slow rollback catches the terminal outcome here without dialing the control port; `null` + when the rig has never taken a control change or the status file is unreadable). - `GET /health` and `GET /tune` — the `rigforge.health` / `rigforge.tune` objects bare. - When XMRig's own API is unreachable the response is still `200` with `"rigforge": {..., "xmrig_api": "unreachable"}` — a down miner is exactly when the health data diff --git a/rigforge.sh b/rigforge.sh index 322fa62..086cd7c 100755 --- a/rigforge.sh +++ b/rigforge.sh @@ -4721,8 +4721,17 @@ _api_config_meta_json() { fi } +# #346: the last control outcome mirrored into the feed as `rigforge.control`. Pithead's host runner +# caps its synchronous /status poll after a worker-apply, so a slow rollback outruns it; rather than a +# new authenticated dial to the control port, the rig mirrors {change_id, status, reason} from the +# status.json _control_status already writes 644, and the next routine poll catches up. Missing, +# unreadable, or malformed status.json -> null — the mirror must never break the feed. +_api_control_json() { + jq -c '{change_id, status, reason}' "${RIGFORGE_CONTROL_STATE:-/var/lib/rigforge-control}/status.json" 2>/dev/null || echo null +} + _api_rigforge_block() { # - jq -n --arg v "$(cat "$SCRIPT_DIR/VERSION" 2>/dev/null || echo unknown)" --arg xv "$XMRIG_VERSION" --arg xc "$XMRIG_COMMIT" --argjson tune "$(_api_tune_json)" --argjson power "$(_api_power_json "$1")" --argjson health "$(_health_json)" --argjson watchdog "$(_watchdog_json)" --argjson config "$(_api_config_json)" --argjson config_meta "$(_api_config_meta_json)" '{version: $v, xmrig_version: $xv, xmrig_commit: $xc, tune: $tune, power: $power, health: $health, watchdog: $watchdog, config: $config, config_meta: $config_meta}' + jq -n --arg v "$(cat "$SCRIPT_DIR/VERSION" 2>/dev/null || echo unknown)" --arg xv "$XMRIG_VERSION" --arg xc "$XMRIG_COMMIT" --argjson tune "$(_api_tune_json)" --argjson power "$(_api_power_json "$1")" --argjson health "$(_health_json)" --argjson watchdog "$(_watchdog_json)" --argjson config "$(_api_config_json)" --argjson config_meta "$(_api_config_meta_json)" --argjson control "$(_api_control_json)" '{version: $v, xmrig_version: $xv, xmrig_commit: $xc, tune: $tune, power: $power, health: $health, watchdog: $watchdog, config: $config, config_meta: $config_meta, control: $control}' } # Produce the sister API's response bodies: compute once, write atomically (tmp + rename, the diff --git a/tests/run.sh b/tests/run.sh index 4b6599f..e84f9ea 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -5979,6 +5979,20 @@ printf '{broken' >"$APIQ/home/worker/tune-overrides.json" run_refresh assert_eq "corrupt tune-overrides -> applied null, not a crash" "$(jq -r '.applied' "$APIQ/data/tune.json")" "null" rm -f "$APIQ/home/worker/tune-overrides.json" +# #346: the last control outcome mirrored into the feed as rigforge.control — pithead's poller catches +# a late terminal outcome on the open read feed instead of a new authenticated dial to the control port. +# A realistic full status.json (the _control_status shape): the mirror picks exactly the three keys. +CTL346="$(mktemp -d "$SANDBOX/ctl346.XXXXXX")" +printf '%s' '{"status":"rolled_back","change_id":"abc0123456789def","source":"control","applied_at":"2026-01-01T00:00:00Z","changed_keys":["DONATION"],"reason":"miner did not return to a live hashrate; rolled back and live","backup":"/b","warnings":[]}' >"$CTL346/status.json" +run_refresh "RIGFORGE_CONTROL_STATE=$CTL346" +assert_eq "control mirror: exactly {change_id, status, reason} on the feed (#346)" "$(jq -cS '.rigforge.control' "$APIQ/data/summary.json")" '{"change_id":"abc0123456789def","reason":"miner did not return to a live hashrate; rolled back and live","status":"rolled_back"}' +# No status.json (a rig that never took a control change, or control disabled) -> null, feed intact. +run_refresh "RIGFORGE_CONTROL_STATE=$CTL346/absent" +assert_eq "control mirror: no status.json -> null (#346)" "$(jq -c '.rigforge.control' "$APIQ/data/summary.json")" "null" +# Malformed status.json -> null, and the refresh still writes the feed. +printf '{broken' >"$CTL346/status.json" +run_refresh "RIGFORGE_CONTROL_STATE=$CTL346" +assert_eq "control mirror: malformed status.json -> null, refresh survives (#346)" "$(jq -c '.rigforge.control' "$APIQ/data/summary.json")" "null" # #276 (item 5): each `printf | jq ... && mv` (rigforge.sh:4233-4235) is independently atomic — a jq # failure on ONE file must not corrupt or block the others. Break _api_rigforge_block so specifically # health.json's own extraction (`.health + {watchdog: .watchdog}`) fails (health is a string, not an